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,
|
||||
title VARCHAR(256) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
content_nomarkup TEXT NOT NULL,
|
||||
author VARCHAR(32) NOT NULL REFERENCES accounts(username),
|
||||
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;
|
@ -38,7 +38,7 @@ impl Banner {
|
||||
Ok(banners)
|
||||
}
|
||||
|
||||
pub async fn read_random(ctx: &Ctx) -> Result<Option<Self>, NekrochanError> {
|
||||
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 {
|
||||
|
@ -26,9 +26,9 @@ impl Board {
|
||||
|
||||
query(&format!(
|
||||
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),
|
||||
thread INT DEFAULT NULL REFERENCES posts_{}(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,
|
||||
@ -114,6 +114,14 @@ impl 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)
|
||||
|
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 banner;
|
||||
mod board;
|
||||
mod local_stats;
|
||||
mod newspost;
|
||||
mod post;
|
||||
|
@ -45,18 +45,11 @@ pub struct Report {
|
||||
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)]
|
||||
pub struct Post {
|
||||
pub id: i32,
|
||||
pub id: i64,
|
||||
pub board: String,
|
||||
pub thread: Option<i32>,
|
||||
pub thread: Option<i64>,
|
||||
pub name: String,
|
||||
pub user_id: String,
|
||||
pub tripcode: Option<String>,
|
||||
@ -89,6 +82,7 @@ pub struct NewsPost {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub content_nomarkup: String,
|
||||
pub author: String,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
@ -104,3 +98,9 @@ pub struct File {
|
||||
pub timestamp: i64,
|
||||
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> {
|
||||
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)
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
ctx: &Ctx,
|
||||
|
@ -12,7 +12,7 @@ impl Post {
|
||||
pub async fn create(
|
||||
ctx: &Ctx,
|
||||
board: &Board,
|
||||
thread: Option<i32>,
|
||||
thread: Option<i64>,
|
||||
name: String,
|
||||
tripcode: Option<String>,
|
||||
capcode: Option<String>,
|
||||
@ -110,7 +110,7 @@ impl Post {
|
||||
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")
|
||||
.bind(board)
|
||||
.bind(id)
|
||||
@ -181,19 +181,6 @@ impl Post {
|
||||
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> {
|
||||
let posts = query_as(
|
||||
r#"SELECT * FROM overboard
|
||||
@ -206,20 +193,6 @@ impl Post {
|
||||
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> {
|
||||
let replies = query_as(&format!(
|
||||
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC",
|
||||
@ -240,6 +213,14 @@ impl Post {
|
||||
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> {
|
||||
query(&format!(
|
||||
"UPDATE posts_{} SET user_id = $1 WHERE id = $2",
|
||||
@ -430,16 +411,16 @@ impl Post {
|
||||
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 (
|
||||
WHERE thread IS NULL AND id NOT IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id
|
||||
FROM posts_{}
|
||||
WHERE thread IS NULL
|
||||
ORDER BY sticky DESC, bumped DESC
|
||||
LIMIT $1
|
||||
) catty
|
||||
)"#,
|
||||
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)
|
||||
|
@ -34,6 +34,8 @@ pub enum NekrochanError {
|
||||
FloodError,
|
||||
#[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)]
|
||||
HeaderError(&'static str),
|
||||
#[error("Domovní stránka vznikne po vytvoření nástěnky.")]
|
||||
HomePageError,
|
||||
#[error("ID musí mít 1-16 znaků.")]
|
||||
IdFormatError,
|
||||
#[error("Nesprávné řešení CAPTCHA.")]
|
||||
@ -41,7 +43,7 @@ pub enum NekrochanError {
|
||||
#[error("Nesprávné přihlašovací údaje.")]
|
||||
IncorrectCredentialError,
|
||||
#[error("Nesprávné heslo pro příspěvek #{}.", .0)]
|
||||
IncorrectPasswordError(i32),
|
||||
IncorrectPasswordError(i64),
|
||||
#[error("Nedostatečná oprávnění.")]
|
||||
InsufficientPermissionError,
|
||||
#[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ů.")]
|
||||
PostNameFormatError,
|
||||
#[error("Příspěvek /{}/{} neexistuje.", .0, .1)]
|
||||
PostNotFound(String, i32),
|
||||
PostNotFound(String, i64),
|
||||
#[error("Vlákno dosáhlo limitu odpovědí.")]
|
||||
ReplyLimitError,
|
||||
#[error("Nelze vytvořit odpověď na odpověď.")]
|
||||
@ -215,6 +217,7 @@ impl ResponseError for NekrochanError {
|
||||
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
|
||||
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
|
||||
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
|
||||
NekrochanError::HomePageError => StatusCode::NOT_FOUND,
|
||||
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
|
||||
NekrochanError::IncorrectCaptchaError => 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 anyhow::Error;
|
||||
use chrono::Utc;
|
||||
use glob::glob;
|
||||
use std::{collections::HashSet, process::Command};
|
||||
use tokio::{
|
||||
fs::{remove_file, rename},
|
||||
task::spawn_blocking,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cfg::Cfg,
|
||||
ctx::Ctx,
|
||||
db::models::{Banner, Board, File, Post},
|
||||
error::NekrochanError,
|
||||
};
|
||||
use crate::{cfg::Cfg, db::models::File, error::NekrochanError};
|
||||
|
||||
impl File {
|
||||
pub async fn new(
|
||||
@ -306,61 +300,3 @@ async fn process_video(
|
||||
|
||||
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> {
|
||||
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();
|
||||
|
||||
Ok(time)
|
||||
|
@ -13,6 +13,7 @@ pub mod ctx;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
pub mod files;
|
||||
pub mod schedule;
|
||||
pub mod filters;
|
||||
pub mod markup;
|
||||
pub mod perms;
|
||||
|
@ -16,7 +16,7 @@ use nekrochan::{
|
||||
ctx::Ctx,
|
||||
db::{cache::init_cache, models::Banner},
|
||||
error::NekrochanError,
|
||||
files::cleanup_files,
|
||||
schedule::s_cleanup_files,
|
||||
web::{self, template_response},
|
||||
};
|
||||
use sqlx::migrate;
|
||||
@ -46,7 +46,7 @@ async fn run() -> Result<(), Error> {
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
match cleanup_files(&ctx_).await {
|
||||
match s_cleanup_files(&ctx_).await {
|
||||
Ok(()) => info!("Routine file cleanup successful."),
|
||||
Err(err) => error!("Routine file cleanup failed: {err:?}"),
|
||||
};
|
||||
|
@ -107,7 +107,7 @@ fn capcode_fallback(owner: bool) -> String {
|
||||
pub async fn markup(
|
||||
ctx: &Ctx,
|
||||
board: &String,
|
||||
op: Option<i32>,
|
||||
op: Option<i64>,
|
||||
text: &str,
|
||||
) -> Result<String, NekrochanError> {
|
||||
let text = escape_html(&text);
|
||||
@ -177,8 +177,8 @@ async fn get_quoted_posts(
|
||||
ctx: &Ctx,
|
||||
board: &String,
|
||||
text: &str,
|
||||
) -> Result<HashMap<i32, Post>, NekrochanError> {
|
||||
let mut quoted_ids: Vec<i32> = Vec::new();
|
||||
) -> Result<HashMap<i64, Post>, NekrochanError> {
|
||||
let mut quoted_ids: Vec<i64> = Vec::new();
|
||||
|
||||
for quote in QUOTE_REGEX.captures_iter(text) {
|
||||
let id_raw = "e[1];
|
||||
|
@ -12,6 +12,7 @@ pub enum Permissions {
|
||||
Bans,
|
||||
BoardBanners,
|
||||
BoardConfig,
|
||||
News,
|
||||
BypassBans,
|
||||
BypassBoardLock,
|
||||
BypassThreadLock,
|
||||
@ -67,6 +68,10 @@ impl PermissionWrapper {
|
||||
self.0.contains(Permissions::BoardConfig)
|
||||
}
|
||||
|
||||
pub fn news(&self) -> bool {
|
||||
self.0.contains(Permissions::News)
|
||||
}
|
||||
|
||||
pub fn bypass_bans(&self) -> bool {
|
||||
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)]
|
||||
pub struct PostForm {
|
||||
pub board: Text<String>,
|
||||
pub thread: Option<Text<i32>>,
|
||||
pub thread: Option<Text<i64>>,
|
||||
pub name: Text<String>,
|
||||
pub email: 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
|
||||
}
|
||||
|
||||
fn parse_id(id: &str) -> Option<(String, i32)> {
|
||||
fn parse_id(id: &str) -> Option<(String, i64)> {
|
||||
let (board, id) = id.split_once('/')?;
|
||||
let board = board.to_owned();
|
||||
let id = id.parse().ok()?;
|
||||
|
@ -2,21 +2,37 @@ use actix_web::{get, web::Data, HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
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)]
|
||||
#[template(path = "index.html")]
|
||||
|
||||
struct IndexTemplate {
|
||||
tcx: TemplateCtx,
|
||||
_news: Vec<NewsPost>,
|
||||
news: Option<NewsPost>,
|
||||
boards: Vec<Board>,
|
||||
stats: LocalStats,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
|
||||
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)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ impl TemplateCtx {
|
||||
};
|
||||
|
||||
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 {
|
||||
cfg,
|
||||
@ -93,15 +93,12 @@ pub async fn account_from_auth_opt(
|
||||
}
|
||||
|
||||
pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> {
|
||||
let ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED);
|
||||
|
||||
// let ip = req
|
||||
// .headers()
|
||||
// .get("X-Real-IP")
|
||||
// .ok_or(NekrochanError::HeaderError("X-Real-IP"))?
|
||||
// .to_str()
|
||||
// .map_err(|_| NekrochanError::HeaderError("X-Real-IP"))?
|
||||
// .parse::<IpAddr>()?;
|
||||
let ip = req
|
||||
.connection_info()
|
||||
.realip_remote_addr()
|
||||
.map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |ip| {
|
||||
ip.parse().unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
|
||||
});
|
||||
|
||||
let country = req.headers().get("X-Country-Code").map_or_else(
|
||||
|| "xx".into(),
|
||||
|
@ -27,7 +27,7 @@ struct ThreadTemplate {
|
||||
pub async fn thread(
|
||||
ctx: Data<Ctx>,
|
||||
req: HttpRequest,
|
||||
path: Path<(String, i32)>,
|
||||
path: Path<(String, i64)>,
|
||||
) -> Result<HttpResponse, NekrochanError> {
|
||||
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 {
|
||||
min-height: 100vh;
|
||||
font-family: var(--font);
|
||||
font-size: 10pt;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@ -107,14 +110,14 @@ summary {
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
background-color: var(--table-primary);
|
||||
border-spacing: 0;
|
||||
border: 1px solid var(--table-border);
|
||||
border-collapse: collapse;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.data-table tr:nth-child(2n + 1) {
|
||||
@ -127,8 +130,8 @@ summary {
|
||||
|
||||
.data-table td,
|
||||
.data-table th {
|
||||
border: 1px solid var(--table-border);
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.data-table .banner {
|
||||
@ -158,6 +161,7 @@ summary {
|
||||
background-color: var(--table-head);
|
||||
font-weight: bold;
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid var(--table-border);
|
||||
}
|
||||
|
||||
.infobox-content {
|
||||
@ -206,15 +210,14 @@ summary {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.8rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
@ -223,6 +226,14 @@ summary {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.float-r {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fixed-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@ -231,9 +242,19 @@ summary {
|
||||
border: 1px solid var(--box-border);
|
||||
}
|
||||
|
||||
.headline {
|
||||
font-size: 1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.headline::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.board-links {
|
||||
color: var(--board-links-color);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.link-separator::after {
|
||||
@ -248,7 +269,11 @@ summary {
|
||||
content: " ] ";
|
||||
}
|
||||
|
||||
.board-links::after {
|
||||
.header {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.header::after {
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
@ -257,6 +282,7 @@ summary {
|
||||
.footer {
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.post {
|
||||
@ -274,8 +300,8 @@ summary {
|
||||
}
|
||||
|
||||
.post::after {
|
||||
display: block;
|
||||
content: "";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@ -359,6 +385,10 @@ summary {
|
||||
font-family: inherit;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post .post-content {
|
||||
margin: 1rem 2rem;
|
||||
}
|
||||
|
||||
@ -370,10 +400,6 @@ summary {
|
||||
color: var(--post-link-hover);
|
||||
}
|
||||
|
||||
.quote {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.dead-quote {
|
||||
color: var(--dead-quote-color);
|
||||
text-decoration: line-through;
|
||||
|
@ -13,11 +13,11 @@
|
||||
<script>0</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="board-links">
|
||||
<div class="board-links header">
|
||||
<span class="link-group"><a href="/">domov</a></span>
|
||||
{% call board_links::board_links() %}
|
||||
<span class="link-group"><a href="/overboard">nadnástěnka</a></span>
|
||||
<span style="float: right;">
|
||||
<span class="float-r">
|
||||
{% if tcx.logged_in %}
|
||||
<span class="link-group"><a href="/logout">odhlásit se</a></span>
|
||||
<span class="link-group"><a href="/staff/account">účet</a></span>
|
||||
@ -28,11 +28,12 @@
|
||||
</div>
|
||||
<div class="main">
|
||||
{% block content %}{% endblock %}
|
||||
<hr>
|
||||
<div class="footer">
|
||||
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
|
||||
<br>
|
||||
<span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span>
|
||||
<div class="box inline-block">
|
||||
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
|
||||
<br>
|
||||
<span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -12,12 +12,12 @@
|
||||
<div class="container">
|
||||
<h1 class="title">Je konec...</h1>
|
||||
|
||||
<div class="infobox">
|
||||
<div class="infobox-head center">
|
||||
<div class="infobox center">
|
||||
<div class="infobox-head">
|
||||
Chyba {{ error_code }}
|
||||
</div>
|
||||
{% if !error_message.is_empty() %}
|
||||
<div class="infobox-content center">
|
||||
<div class="infobox-content">
|
||||
{{ error_message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -5,8 +5,57 @@
|
||||
{% block title %}{{ tcx.cfg.site.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="center">
|
||||
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
||||
<p class="description">{{ tcx.cfg.site.description }}</p>
|
||||
<div class="container">
|
||||
<div class="center">
|
||||
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Účty</h2>
|
||||
<form method="post" action="/staff/actions/remove-accounts">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<table class="data-table center">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Jméno</th>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Bannery</h2>
|
||||
<form method="post" action="/staff/actions/remove-banners">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<table class="data-table center">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Banner</th>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Bany</h2>
|
||||
<form method="post" action="/staff/actions/remove-bans">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<table class="data-table center">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>IP</th>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<h2>Nástěnky</h2>
|
||||
<form method="post">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<table class="data-table center">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>ID</th>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<hr>
|
||||
<h2>Záznamy</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<table class="data-table center">
|
||||
<tr>
|
||||
<th>Zpráva</th>
|
||||
<th>Datum</th>
|
||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele