Nová domovní stránka
Tento commit je obsažen v:
rodič
6f4403e376
revize
11bba18d93
@ -2,6 +2,7 @@ CREATE TABLE news (
|
|||||||
id SERIAL NOT NULL PRIMARY KEY,
|
id SERIAL NOT NULL PRIMARY KEY,
|
||||||
title VARCHAR(256) NOT NULL,
|
title VARCHAR(256) NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
|
content_nomarkup TEXT NOT NULL,
|
||||||
author VARCHAR(32) NOT NULL REFERENCES accounts(username),
|
author VARCHAR(32) NOT NULL REFERENCES accounts(username),
|
||||||
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
2
migrations/20231229180942_remove_references.sql
Normální soubor
2
migrations/20231229180942_remove_references.sql
Normální soubor
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE bans DROP CONSTRAINT bans_issued_by_fkey;
|
||||||
|
ALTER TABLE news DROP CONSTRAINT news_author_fkey;
|
@ -26,9 +26,9 @@ impl Board {
|
|||||||
|
|
||||||
query(&format!(
|
query(&format!(
|
||||||
r#"CREATE TABLE posts_{} (
|
r#"CREATE TABLE posts_{} (
|
||||||
id SERIAL NOT NULL PRIMARY KEY,
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id),
|
board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id),
|
||||||
thread INT DEFAULT NULL REFERENCES posts_{}(id),
|
thread BIGINT DEFAULT NULL REFERENCES posts_{}(id),
|
||||||
name VARCHAR(32) NOT NULL,
|
name VARCHAR(32) NOT NULL,
|
||||||
user_id VARCHAR(6) NOT NULL DEFAULT '000000',
|
user_id VARCHAR(6) NOT NULL DEFAULT '000000',
|
||||||
tripcode VARCHAR(12) DEFAULT NULL,
|
tripcode VARCHAR(12) DEFAULT NULL,
|
||||||
@ -114,6 +114,14 @@ impl Board {
|
|||||||
Ok(boards)
|
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> {
|
pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> {
|
||||||
query("UPDATE boards SET name = $1 WHERE id = $2")
|
query("UPDATE boards SET name = $1 WHERE id = $2")
|
||||||
.bind(&name)
|
.bind(&name)
|
||||||
|
30
src/db/local_stats.rs
Normální soubor
30
src/db/local_stats.rs
Normální soubor
@ -0,0 +1,30 @@
|
|||||||
|
use sqlx::query_as;
|
||||||
|
|
||||||
|
use crate::{error::NekrochanError, ctx::Ctx};
|
||||||
|
use super::models::LocalStats;
|
||||||
|
|
||||||
|
impl LocalStats {
|
||||||
|
pub async fn read(ctx: &Ctx) -> Result<Self, NekrochanError> {
|
||||||
|
let (post_count,) = query_as(
|
||||||
|
"SELECT coalesce(sum(last_value)::bigint, 0) FROM pg_sequences WHERE sequencename LIKE 'posts_%_id_seq'",
|
||||||
|
)
|
||||||
|
.fetch_one(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let (file_count, file_size) = query_as(
|
||||||
|
r#"SELECT count(files), coalesce(sum((files->>'size')::bigint)::bigint, 0) FROM (
|
||||||
|
SELECT jsonb_array_elements(files) AS files FROM overboard
|
||||||
|
) flatten"#,
|
||||||
|
)
|
||||||
|
.fetch_one(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let stats = Self {
|
||||||
|
post_count,
|
||||||
|
file_count,
|
||||||
|
file_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
}
|
@ -5,5 +5,6 @@ mod account;
|
|||||||
mod ban;
|
mod ban;
|
||||||
mod banner;
|
mod banner;
|
||||||
mod board;
|
mod board;
|
||||||
|
mod local_stats;
|
||||||
mod newspost;
|
mod newspost;
|
||||||
mod post;
|
mod post;
|
||||||
|
@ -45,18 +45,11 @@ pub struct Report {
|
|||||||
pub reporter_ip: IpAddr,
|
pub reporter_ip: IpAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize)]
|
|
||||||
pub struct LogRecord {
|
|
||||||
pub id: i32,
|
|
||||||
pub message: String,
|
|
||||||
pub created: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize)]
|
#[derive(FromRow, Serialize, Deserialize)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: i32,
|
pub id: i64,
|
||||||
pub board: String,
|
pub board: String,
|
||||||
pub thread: Option<i32>,
|
pub thread: Option<i64>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
pub tripcode: Option<String>,
|
pub tripcode: Option<String>,
|
||||||
@ -89,6 +82,7 @@ pub struct NewsPost {
|
|||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub content_nomarkup: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
@ -104,3 +98,9 @@ pub struct File {
|
|||||||
pub timestamp: i64,
|
pub timestamp: i64,
|
||||||
pub size: usize,
|
pub size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct LocalStats {
|
||||||
|
pub post_count: i64,
|
||||||
|
pub file_count: i64,
|
||||||
|
pub file_size: i64,
|
||||||
|
}
|
||||||
|
@ -30,11 +30,21 @@ impl NewsPost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||||
let newsposts = query_as("SELECT * FROM news").fetch_all(ctx.db()).await?;
|
let newsposts = query_as("SELECT * FROM news ORDER BY created DESC")
|
||||||
|
.fetch_all(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(newsposts)
|
Ok(newsposts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn read_latest(ctx: &Ctx) -> Result<Option<Self>, NekrochanError> {
|
||||||
|
let newspost = query_as("SELECT * FROM news ORDER BY created DESC LIMIT 1")
|
||||||
|
.fetch_optional(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(newspost)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
&self,
|
&self,
|
||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
|
@ -12,7 +12,7 @@ impl Post {
|
|||||||
pub async fn create(
|
pub async fn create(
|
||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
board: &Board,
|
board: &Board,
|
||||||
thread: Option<i32>,
|
thread: Option<i64>,
|
||||||
name: String,
|
name: String,
|
||||||
tripcode: Option<String>,
|
tripcode: Option<String>,
|
||||||
capcode: Option<String>,
|
capcode: Option<String>,
|
||||||
@ -110,7 +110,7 @@ impl Post {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read(ctx: &Ctx, board: String, id: i32) -> Result<Option<Self>, NekrochanError> {
|
pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
|
||||||
let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2")
|
let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2")
|
||||||
.bind(board)
|
.bind(board)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
@ -181,19 +181,6 @@ impl Post {
|
|||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_latest(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
|
||||||
let posts = query_as(
|
|
||||||
r#"SELECT * FROM overboard
|
|
||||||
ORDER BY created DESC
|
|
||||||
LIMIT $1"#,
|
|
||||||
)
|
|
||||||
.bind(15)
|
|
||||||
.fetch_all(ctx.db())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(posts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_reports(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
pub async fn read_reports(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||||
let posts = query_as(
|
let posts = query_as(
|
||||||
r#"SELECT * FROM overboard
|
r#"SELECT * FROM overboard
|
||||||
@ -206,20 +193,6 @@ impl Post {
|
|||||||
Ok(posts)
|
Ok(posts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn read_files(ctx: &Ctx) -> Result<Vec<Post>, NekrochanError> {
|
|
||||||
let posts = query_as(
|
|
||||||
r#"SELECT *
|
|
||||||
FROM overboard
|
|
||||||
WHERE files != '[]'::jsonb
|
|
||||||
ORDER BY created DESC
|
|
||||||
LIMIT 3"#,
|
|
||||||
)
|
|
||||||
.fetch_all(ctx.db())
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(posts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||||
let replies = query_as(&format!(
|
let replies = query_as(&format!(
|
||||||
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC",
|
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC",
|
||||||
@ -240,6 +213,14 @@ impl Post {
|
|||||||
Ok(posts)
|
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 update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> {
|
pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> {
|
||||||
query(&format!(
|
query(&format!(
|
||||||
"UPDATE posts_{} SET user_id = $1 WHERE id = $2",
|
"UPDATE posts_{} SET user_id = $1 WHERE id = $2",
|
||||||
|
@ -34,6 +34,8 @@ pub enum NekrochanError {
|
|||||||
FloodError,
|
FloodError,
|
||||||
#[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)]
|
#[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)]
|
||||||
HeaderError(&'static str),
|
HeaderError(&'static str),
|
||||||
|
#[error("Domovní stránka vznikne po vytvoření nástěnky.")]
|
||||||
|
HomePageError,
|
||||||
#[error("ID musí mít 1-16 znaků.")]
|
#[error("ID musí mít 1-16 znaků.")]
|
||||||
IdFormatError,
|
IdFormatError,
|
||||||
#[error("Nesprávné řešení CAPTCHA.")]
|
#[error("Nesprávné řešení CAPTCHA.")]
|
||||||
@ -41,7 +43,7 @@ pub enum NekrochanError {
|
|||||||
#[error("Nesprávné přihlašovací údaje.")]
|
#[error("Nesprávné přihlašovací údaje.")]
|
||||||
IncorrectCredentialError,
|
IncorrectCredentialError,
|
||||||
#[error("Nesprávné heslo pro příspěvek #{}.", .0)]
|
#[error("Nesprávné heslo pro příspěvek #{}.", .0)]
|
||||||
IncorrectPasswordError(i32),
|
IncorrectPasswordError(i64),
|
||||||
#[error("Nedostatečná oprávnění.")]
|
#[error("Nedostatečná oprávnění.")]
|
||||||
InsufficientPermissionError,
|
InsufficientPermissionError,
|
||||||
#[error("Server se připojil k 41 procentům.")]
|
#[error("Server se připojil k 41 procentům.")]
|
||||||
@ -67,7 +69,7 @@ pub enum NekrochanError {
|
|||||||
#[error("Jméno nesmí mít více než 32 znaků.")]
|
#[error("Jméno nesmí mít více než 32 znaků.")]
|
||||||
PostNameFormatError,
|
PostNameFormatError,
|
||||||
#[error("Příspěvek /{}/{} neexistuje.", .0, .1)]
|
#[error("Příspěvek /{}/{} neexistuje.", .0, .1)]
|
||||||
PostNotFound(String, i32),
|
PostNotFound(String, i64),
|
||||||
#[error("Vlákno dosáhlo limitu odpovědí.")]
|
#[error("Vlákno dosáhlo limitu odpovědí.")]
|
||||||
ReplyLimitError,
|
ReplyLimitError,
|
||||||
#[error("Nelze vytvořit odpověď na odpověď.")]
|
#[error("Nelze vytvořit odpověď na odpověď.")]
|
||||||
@ -215,6 +217,7 @@ impl ResponseError for NekrochanError {
|
|||||||
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
|
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
|
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
|
||||||
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
|
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
|
||||||
|
NekrochanError::HomePageError => StatusCode::NOT_FOUND,
|
||||||
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED,
|
NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED,
|
||||||
NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED,
|
NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED,
|
||||||
|
70
src/files.rs
70
src/files.rs
@ -1,19 +1,13 @@
|
|||||||
|
use std::process::Command;
|
||||||
|
|
||||||
use actix_multipart::form::tempfile::TempFile;
|
use actix_multipart::form::tempfile::TempFile;
|
||||||
use anyhow::Error;
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use glob::glob;
|
|
||||||
use std::{collections::HashSet, process::Command};
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{remove_file, rename},
|
fs::{remove_file, rename},
|
||||||
task::spawn_blocking,
|
task::spawn_blocking,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{cfg::Cfg, db::models::File, error::NekrochanError};
|
||||||
cfg::Cfg,
|
|
||||||
ctx::Ctx,
|
|
||||||
db::models::{Banner, Board, File, Post},
|
|
||||||
error::NekrochanError,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl File {
|
impl File {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
@ -306,61 +300,3 @@ async fn process_video(
|
|||||||
|
|
||||||
Ok((width, height))
|
Ok((width, height))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
|
||||||
let posts = Post::read_all(ctx, board.id.clone()).await?;
|
|
||||||
|
|
||||||
for post in posts {
|
|
||||||
for file in post.files.0 {
|
|
||||||
keep.insert(format!("{}.{}", file.timestamp, file.format));
|
|
||||||
|
|
||||||
if let Some(thumb_format) = file.thumb_format {
|
|
||||||
keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for file in glob("./uploads/*.*")? {
|
|
||||||
let file = file?;
|
|
||||||
let file_name = file.file_name();
|
|
||||||
|
|
||||||
if let Some(file_name) = file_name {
|
|
||||||
let check = file_name.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
if !keep.contains(&check) {
|
|
||||||
remove_file(file).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for file in glob("./uploads/thumb/*.*")? {
|
|
||||||
let file = file?;
|
|
||||||
let file_name = file.file_name();
|
|
||||||
|
|
||||||
if let Some(file_name) = file_name {
|
|
||||||
let check = file_name.to_string_lossy().to_string();
|
|
||||||
|
|
||||||
if !keep_thumbs.contains(&check) {
|
|
||||||
remove_file(file).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
@ -68,7 +68,7 @@ pub fn czech_humantime(time: &DateTime<Utc>) -> askama::Result<String> {
|
|||||||
|
|
||||||
pub fn czech_datetime(time: &DateTime<Utc>) -> askama::Result<String> {
|
pub fn czech_datetime(time: &DateTime<Utc>) -> askama::Result<String> {
|
||||||
let time = time
|
let time = time
|
||||||
.format_localized("%d.%m.%Y (%a) %H:%M:%S UTC", Locale::cs_CZ)
|
.format_localized("%d.%m.%Y (%a) %H:%M:%S", Locale::cs_CZ)
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
Ok(time)
|
Ok(time)
|
||||||
|
@ -13,6 +13,7 @@ pub mod ctx;
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
|
pub mod schedule;
|
||||||
pub mod filters;
|
pub mod filters;
|
||||||
pub mod markup;
|
pub mod markup;
|
||||||
pub mod perms;
|
pub mod perms;
|
||||||
|
@ -16,7 +16,7 @@ use nekrochan::{
|
|||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::{cache::init_cache, models::Banner},
|
db::{cache::init_cache, models::Banner},
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
files::cleanup_files,
|
schedule::s_cleanup_files,
|
||||||
web::{self, template_response},
|
web::{self, template_response},
|
||||||
};
|
};
|
||||||
use sqlx::migrate;
|
use sqlx::migrate;
|
||||||
@ -46,7 +46,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
match cleanup_files(&ctx_).await {
|
match s_cleanup_files(&ctx_).await {
|
||||||
Ok(()) => info!("Routine file cleanup successful."),
|
Ok(()) => info!("Routine file cleanup successful."),
|
||||||
Err(err) => error!("Routine file cleanup failed: {err:?}"),
|
Err(err) => error!("Routine file cleanup failed: {err:?}"),
|
||||||
};
|
};
|
||||||
|
@ -107,7 +107,7 @@ fn capcode_fallback(owner: bool) -> String {
|
|||||||
pub async fn markup(
|
pub async fn markup(
|
||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
board: &String,
|
board: &String,
|
||||||
op: Option<i32>,
|
op: Option<i64>,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Result<String, NekrochanError> {
|
) -> Result<String, NekrochanError> {
|
||||||
let text = escape_html(&text);
|
let text = escape_html(&text);
|
||||||
@ -177,8 +177,8 @@ async fn get_quoted_posts(
|
|||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
board: &String,
|
board: &String,
|
||||||
text: &str,
|
text: &str,
|
||||||
) -> Result<HashMap<i32, Post>, NekrochanError> {
|
) -> Result<HashMap<i64, Post>, NekrochanError> {
|
||||||
let mut quoted_ids: Vec<i32> = Vec::new();
|
let mut quoted_ids: Vec<i64> = Vec::new();
|
||||||
|
|
||||||
for quote in QUOTE_REGEX.captures_iter(text) {
|
for quote in QUOTE_REGEX.captures_iter(text) {
|
||||||
let id_raw = "e[1];
|
let id_raw = "e[1];
|
||||||
|
@ -12,6 +12,7 @@ pub enum Permissions {
|
|||||||
Bans,
|
Bans,
|
||||||
BoardBanners,
|
BoardBanners,
|
||||||
BoardConfig,
|
BoardConfig,
|
||||||
|
News,
|
||||||
BypassBans,
|
BypassBans,
|
||||||
BypassBoardLock,
|
BypassBoardLock,
|
||||||
BypassThreadLock,
|
BypassThreadLock,
|
||||||
@ -67,6 +68,10 @@ impl PermissionWrapper {
|
|||||||
self.0.contains(Permissions::BoardConfig)
|
self.0.contains(Permissions::BoardConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn news(&self) -> bool {
|
||||||
|
self.0.contains(Permissions::News)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bypass_bans(&self) -> bool {
|
pub fn bypass_bans(&self) -> bool {
|
||||||
self.0.contains(Permissions::BypassBans)
|
self.0.contains(Permissions::BypassBans)
|
||||||
}
|
}
|
||||||
|
67
src/schedule.rs
Normální soubor
67
src/schedule.rs
Normální soubor
@ -0,0 +1,67 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use glob::glob;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use tokio::fs::remove_file;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ctx::Ctx,
|
||||||
|
db::models::{Banner, Board, Post},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn s_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 {
|
||||||
|
let posts = Post::read_all(ctx, board.id.clone()).await?;
|
||||||
|
|
||||||
|
for post in posts {
|
||||||
|
for file in post.files.0 {
|
||||||
|
keep.insert(format!("{}.{}", file.timestamp, file.format));
|
||||||
|
|
||||||
|
if let Some(thumb_format) = file.thumb_format {
|
||||||
|
keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in glob("./uploads/*.*")? {
|
||||||
|
let file = file?;
|
||||||
|
let file_name = file.file_name();
|
||||||
|
|
||||||
|
if let Some(file_name) = file_name {
|
||||||
|
let check = file_name.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if !keep.contains(&check) {
|
||||||
|
remove_file(file).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for file in glob("./uploads/thumb/*.*")? {
|
||||||
|
let file = file?;
|
||||||
|
let file_name = file.file_name();
|
||||||
|
|
||||||
|
if let Some(file_name) = file_name {
|
||||||
|
let check = file_name.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if !keep_thumbs.contains(&check) {
|
||||||
|
remove_file(file).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -25,7 +25,7 @@ use crate::{
|
|||||||
#[derive(MultipartForm)]
|
#[derive(MultipartForm)]
|
||||||
pub struct PostForm {
|
pub struct PostForm {
|
||||||
pub board: Text<String>,
|
pub board: Text<String>,
|
||||||
pub thread: Option<Text<i32>>,
|
pub thread: Option<Text<i64>>,
|
||||||
pub name: Text<String>,
|
pub name: Text<String>,
|
||||||
pub email: Text<String>,
|
pub email: Text<String>,
|
||||||
pub content: Text<String>,
|
pub content: Text<String>,
|
||||||
|
@ -31,7 +31,7 @@ pub async fn get_posts_from_ids(ctx: &Ctx, ids: Vec<String>) -> Vec<Post> {
|
|||||||
posts
|
posts
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_id(id: &str) -> Option<(String, i32)> {
|
fn parse_id(id: &str) -> Option<(String, i64)> {
|
||||||
let (board, id) = id.split_once('/')?;
|
let (board, id) = id.split_once('/')?;
|
||||||
let board = board.to_owned();
|
let board = board.to_owned();
|
||||||
let id = id.parse().ok()?;
|
let id = id.parse().ok()?;
|
||||||
|
@ -2,21 +2,37 @@ use actix_web::{get, web::Data, HttpRequest, HttpResponse};
|
|||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
use super::tcx::TemplateCtx;
|
use super::tcx::TemplateCtx;
|
||||||
use crate::{ctx::Ctx, db::models::NewsPost, error::NekrochanError, web::template_response};
|
use crate::{
|
||||||
|
ctx::Ctx,
|
||||||
|
db::models::{Board, LocalStats, NewsPost},
|
||||||
|
error::NekrochanError,
|
||||||
|
filters,
|
||||||
|
web::template_response,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
|
|
||||||
struct IndexTemplate {
|
struct IndexTemplate {
|
||||||
tcx: TemplateCtx,
|
tcx: TemplateCtx,
|
||||||
_news: Vec<NewsPost>,
|
news: Option<NewsPost>,
|
||||||
|
boards: Vec<Board>,
|
||||||
|
stats: LocalStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||||
let _news = NewsPost::read_all(&ctx).await?;
|
|
||||||
let template = IndexTemplate { tcx, _news };
|
if tcx.boards.is_empty() {
|
||||||
|
return Err(NekrochanError::HomePageError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let news = NewsPost::read_latest(&ctx).await?;
|
||||||
|
let boards = Board::read_all(&ctx).await?;
|
||||||
|
let stats = LocalStats::read(&ctx).await?;
|
||||||
|
|
||||||
|
let template = IndexTemplate { tcx, boards, stats, news };
|
||||||
|
|
||||||
template_response(&template)
|
template_response(&template)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ impl TemplateCtx {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let (ip, _) = ip_from_req(req)?;
|
let (ip, _) = ip_from_req(req)?;
|
||||||
let yous = ctx.cache().zrange(format!("yous:{ip}"), 0, -1).await?;
|
let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?;
|
||||||
|
|
||||||
let tcx = Self {
|
let tcx = Self {
|
||||||
cfg,
|
cfg,
|
||||||
@ -93,15 +93,12 @@ pub async fn account_from_auth_opt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> {
|
pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> {
|
||||||
let ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED);
|
let ip = req
|
||||||
|
.connection_info()
|
||||||
// let ip = req
|
.realip_remote_addr()
|
||||||
// .headers()
|
.map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |ip| {
|
||||||
// .get("X-Real-IP")
|
ip.parse().unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
|
||||||
// .ok_or(NekrochanError::HeaderError("X-Real-IP"))?
|
});
|
||||||
// .to_str()
|
|
||||||
// .map_err(|_| NekrochanError::HeaderError("X-Real-IP"))?
|
|
||||||
// .parse::<IpAddr>()?;
|
|
||||||
|
|
||||||
let country = req.headers().get("X-Country-Code").map_or_else(
|
let country = req.headers().get("X-Country-Code").map_or_else(
|
||||||
|| "xx".into(),
|
|| "xx".into(),
|
||||||
|
@ -27,7 +27,7 @@ struct ThreadTemplate {
|
|||||||
pub async fn thread(
|
pub async fn thread(
|
||||||
ctx: Data<Ctx>,
|
ctx: Data<Ctx>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
path: Path<(String, i32)>,
|
path: Path<(String, i64)>,
|
||||||
) -> Result<HttpResponse, NekrochanError> {
|
) -> Result<HttpResponse, NekrochanError> {
|
||||||
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||||
|
|
||||||
|
binární
static/favicon.ico
binární
static/favicon.ico
Binární soubor nebyl zobrazen.
Před Šířka: | Výška: | Velikost: 4.2 KiB Za Šířka: | Výška: | Velikost: 766 B |
binární
static/spoiler.png
binární
static/spoiler.png
Binární soubor nebyl zobrazen.
Před Šířka: | Výška: | Velikost: 2.6 KiB Za Šířka: | Výška: | Velikost: 1.3 KiB |
@ -1,9 +1,12 @@
|
|||||||
|
:root {
|
||||||
|
font-size: 10pt;
|
||||||
|
font-family: var(--font);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 10pt;
|
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +110,14 @@ summary {
|
|||||||
|
|
||||||
.table-wrap {
|
.table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: var(--table-primary);
|
background-color: var(--table-primary);
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border: 1px solid var(--table-border);
|
border-collapse: collapse;
|
||||||
|
margin: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table tr:nth-child(2n + 1) {
|
.data-table tr:nth-child(2n + 1) {
|
||||||
@ -127,8 +130,8 @@ summary {
|
|||||||
|
|
||||||
.data-table td,
|
.data-table td,
|
||||||
.data-table th {
|
.data-table th {
|
||||||
|
border: 1px solid var(--table-border);
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-table .banner {
|
.data-table .banner {
|
||||||
@ -158,6 +161,7 @@ summary {
|
|||||||
background-color: var(--table-head);
|
background-color: var(--table-head);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
border-bottom: 1px solid var(--table-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infobox-content {
|
.infobox-content {
|
||||||
@ -206,15 +210,14 @@ summary {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.big {
|
.big {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
@ -223,6 +226,14 @@ summary {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.float-r {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fixed-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -231,9 +242,19 @@ summary {
|
|||||||
border: 1px solid var(--box-border);
|
border: 1px solid var(--box-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headline {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headline::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
.board-links {
|
.board-links {
|
||||||
color: var(--board-links-color);
|
color: var(--board-links-color);
|
||||||
padding: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.link-separator::after {
|
.link-separator::after {
|
||||||
@ -248,7 +269,11 @@ summary {
|
|||||||
content: " ] ";
|
content: " ] ";
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-links::after {
|
.header {
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header::after {
|
||||||
content: "";
|
content: "";
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
@ -257,6 +282,7 @@ summary {
|
|||||||
.footer {
|
.footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 8pt;
|
font-size: 8pt;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
@ -274,8 +300,8 @@ summary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post::after {
|
.post::after {
|
||||||
display: block;
|
|
||||||
content: "";
|
content: "";
|
||||||
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -359,6 +385,10 @@ summary {
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post .post-content {
|
||||||
margin: 1rem 2rem;
|
margin: 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,10 +400,6 @@ summary {
|
|||||||
color: var(--post-link-hover);
|
color: var(--post-link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quote {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dead-quote {
|
.dead-quote {
|
||||||
color: var(--dead-quote-color);
|
color: var(--dead-quote-color);
|
||||||
text-decoration: line-through;
|
text-decoration: line-through;
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
<script>0</script>
|
<script>0</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="board-links">
|
<div class="board-links header">
|
||||||
<span class="link-group"><a href="/">domov</a></span>
|
<span class="link-group"><a href="/">domov</a></span>
|
||||||
{% call board_links::board_links() %}
|
{% call board_links::board_links() %}
|
||||||
<span class="link-group"><a href="/overboard">nadnástěnka</a></span>
|
<span class="link-group"><a href="/overboard">nadnástěnka</a></span>
|
||||||
<span style="float: right;">
|
<span class="float-r">
|
||||||
{% if tcx.logged_in %}
|
{% if tcx.logged_in %}
|
||||||
<span class="link-group"><a href="/logout">odhlásit se</a></span>
|
<span class="link-group"><a href="/logout">odhlásit se</a></span>
|
||||||
<span class="link-group"><a href="/staff/account">účet</a></span>
|
<span class="link-group"><a href="/staff/account">účet</a></span>
|
||||||
@ -28,12 +28,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
<hr>
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
|
<div class="box inline-block">
|
||||||
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
|
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
|
||||||
<br>
|
<br>
|
||||||
<span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span>
|
<span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -12,12 +12,12 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h1 class="title">Je konec...</h1>
|
<h1 class="title">Je konec...</h1>
|
||||||
|
|
||||||
<div class="infobox">
|
<div class="infobox center">
|
||||||
<div class="infobox-head center">
|
<div class="infobox-head">
|
||||||
Chyba {{ error_code }}
|
Chyba {{ error_code }}
|
||||||
</div>
|
</div>
|
||||||
{% if !error_message.is_empty() %}
|
{% if !error_message.is_empty() %}
|
||||||
<div class="infobox-content center">
|
<div class="infobox-content">
|
||||||
{{ error_message }}
|
{{ error_message }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,8 +5,57 @@
|
|||||||
{% block title %}{{ tcx.cfg.site.name }}{% endblock %}
|
{% block title %}{{ tcx.cfg.site.name }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="center">
|
<div class="container">
|
||||||
|
<div class="center">
|
||||||
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
||||||
<p class="description">{{ tcx.cfg.site.description }}</p>
|
<p class="description">{{ tcx.cfg.site.description }}</p>
|
||||||
|
<p class="board-links big">{% call board_links::board_links() %}</p>
|
||||||
|
</div>
|
||||||
|
{% if let Some(news) = news %}
|
||||||
|
<table class="data-table">
|
||||||
|
<tr>
|
||||||
|
<th>Novinky</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<h2 class="headline">
|
||||||
|
<span>{{ news.title }}</span>
|
||||||
|
<span class="float-r">{{ news.author }} - {{ news.created|czech_datetime }}</span>
|
||||||
|
</h2>
|
||||||
|
<hr>
|
||||||
|
<pre class="post-content">{{ news.content|safe }}</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="/news">Zobrazit všechny novinky...</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
<table class="data-table fixed-table center">
|
||||||
|
<tr>
|
||||||
|
<th>Nástěnky</th>
|
||||||
|
<th>Statistika</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<ul class="infobox-list">
|
||||||
|
{% for board in boards %}
|
||||||
|
<li><a href="/boards/{{ board.id }}">/{{ board.id }}/ - {{ board.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% let board_count = tcx.boards.len() %}
|
||||||
|
Celkem {{ "byl vytvořen|byly vytvořeny|bylo vytvořeno"|czech_plural(stats.post_count) }} 
|
||||||
|
<b>{{ stats.post_count }}</b> {{ "příspěvek|příspěvky|příspěvků"|czech_plural(stats.post_count) }} 
|
||||||
|
na <b>{{ board_count }}</b> {{ "nástěnce|nástěnkách|nástěnkách"|czech_plural(board_count) }}.
|
||||||
|
<br>
|
||||||
|
Aktuálně {{ "je nahrán|jsou nahrány|je nahráno"|czech_plural(stats.file_count) }} <b>{{ stats.file_count }}</b> 
|
||||||
|
{{ "soubor|soubory|souborů"|czech_plural(stats.file_count) }}, celkem <b>{{ stats.file_size|filesizeformat }}</b>.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<h2>Účty</h2>
|
<h2>Účty</h2>
|
||||||
<form method="post" action="/staff/actions/remove-accounts">
|
<form method="post" action="/staff/actions/remove-accounts">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table center">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Jméno</th>
|
<th>Jméno</th>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<h2>Bannery</h2>
|
<h2>Bannery</h2>
|
||||||
<form method="post" action="/staff/actions/remove-banners">
|
<form method="post" action="/staff/actions/remove-banners">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table center">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>Banner</th>
|
<th>Banner</th>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<h2>Bany</h2>
|
<h2>Bany</h2>
|
||||||
<form method="post" action="/staff/actions/remove-bans">
|
<form method="post" action="/staff/actions/remove-bans">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table center">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>IP</th>
|
<th>IP</th>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<h2>Nástěnky</h2>
|
<h2>Nástěnky</h2>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table center">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
<h2>Záznamy</h2>
|
<h2>Záznamy</h2>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table center">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Zpráva</th>
|
<th>Zpráva</th>
|
||||||
<th>Datum</th>
|
<th>Datum</th>
|
||||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele