Skript na živé aktualizace vláken + relativní časy na frontendu
Tento commit je obsažen v:
rodič
cc6921d4ed
revize
3cce9a32e1
82
Cargo.lock
vygenerováno
82
Cargo.lock
vygenerováno
@ -2,6 +2,31 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix"
|
||||||
|
version = "0.13.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fb72882332b6d6282f428b77ba0358cb2687e61a6f6df6a6d3871e8a177c2d4f"
|
||||||
|
dependencies = [
|
||||||
|
"actix-macros",
|
||||||
|
"actix-rt",
|
||||||
|
"actix_derive",
|
||||||
|
"bitflags 2.3.3",
|
||||||
|
"bytes",
|
||||||
|
"crossbeam-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
|
"pin-project-lite",
|
||||||
|
"smallvec",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-codec"
|
name = "actix-codec"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@ -232,6 +257,24 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-web-actors"
|
||||||
|
version = "4.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "420b001bb709d8510c3e2659dae046e54509ff9528018d09c78381e765a1f9fa"
|
||||||
|
dependencies = [
|
||||||
|
"actix",
|
||||||
|
"actix-codec",
|
||||||
|
"actix-http",
|
||||||
|
"actix-web",
|
||||||
|
"bytes",
|
||||||
|
"bytestring",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "4.2.0"
|
version = "4.2.0"
|
||||||
@ -244,6 +287,17 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix_derive"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c7db3d5a9718568e4cf4a537cfd7070e6e6ff7481510d0237fb529ac850f6d3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.28",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
@ -753,6 +807,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-queue"
|
name = "crossbeam-queue"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -765,12 +828,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.16"
|
version = "0.8.19"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
|
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crypto-common"
|
||||||
@ -1732,9 +1792,11 @@ dependencies = [
|
|||||||
name = "nekrochan"
|
name = "nekrochan"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"actix",
|
||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-multipart",
|
"actix-multipart",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"actix-web-actors",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"askama",
|
"askama",
|
||||||
"captcha",
|
"captcha",
|
||||||
@ -1764,6 +1826,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml",
|
"toml",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3052,6 +3115,15 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uuid"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -4,9 +4,11 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
actix = "0.13.3"
|
||||||
actix-files = "0.6.2"
|
actix-files = "0.6.2"
|
||||||
actix-multipart = "0.6.0"
|
actix-multipart = "0.6.0"
|
||||||
actix-web = { version = "4.3.1", features = ["cookies"] }
|
actix-web = { version = "4.3.1", features = ["cookies"] }
|
||||||
|
actix-web-actors = "4.3.0"
|
||||||
askama = "0.12.0"
|
askama = "0.12.0"
|
||||||
anyhow = "1.0.71"
|
anyhow = "1.0.71"
|
||||||
captcha = "0.0.9"
|
captcha = "0.0.9"
|
||||||
@ -40,6 +42,7 @@ sqlx = { version = "0.7.0", features = [
|
|||||||
thiserror = "1.0.41"
|
thiserror = "1.0.41"
|
||||||
tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.29.1", features = ["rt-multi-thread", "macros"] }
|
||||||
toml = "0.8.6"
|
toml = "0.8.6"
|
||||||
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.74"
|
anyhow = "1.0.74"
|
||||||
|
12
src/cfg.rs
12
src/cfg.rs
@ -2,7 +2,7 @@ use anyhow::Error;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::fs::read_to_string;
|
use tokio::fs::read_to_string;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct Cfg {
|
pub struct Cfg {
|
||||||
pub server: ServerCfg,
|
pub server: ServerCfg,
|
||||||
pub site: SiteCfg,
|
pub site: SiteCfg,
|
||||||
@ -20,28 +20,28 @@ impl Cfg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct ServerCfg {
|
pub struct ServerCfg {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub database_url: String,
|
pub database_url: String,
|
||||||
pub cache_url: String,
|
pub cache_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct SiteCfg {
|
pub struct SiteCfg {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub default_noko: bool,
|
pub default_noko: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct SecretsCfg {
|
pub struct SecretsCfg {
|
||||||
pub auth_token: String,
|
pub auth_token: String,
|
||||||
pub secure_trip: String,
|
pub secure_trip: String,
|
||||||
pub user_id: String,
|
pub user_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Debug, Clone)]
|
||||||
pub struct FilesCfg {
|
pub struct FilesCfg {
|
||||||
pub videos: bool,
|
pub videos: bool,
|
||||||
pub thumb_size: u32,
|
pub thumb_size: u32,
|
||||||
@ -51,7 +51,7 @@ pub struct FilesCfg {
|
|||||||
pub cleanup_interval: u64,
|
pub cleanup_interval: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct BoardCfg {
|
pub struct BoardCfg {
|
||||||
pub anon_name: String,
|
pub anon_name: String,
|
||||||
pub page_size: i64,
|
pub page_size: i64,
|
||||||
|
22
src/ctx.rs
22
src/ctx.rs
@ -1,25 +1,33 @@
|
|||||||
|
use actix::{Actor, Addr};
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use redis::{aio::MultiplexedConnection, Client};
|
use redis::{aio::MultiplexedConnection, Client};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use crate::cfg::Cfg;
|
use crate::{cfg::Cfg, live_hub::LiveHub};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Ctx {
|
pub struct Ctx {
|
||||||
pub cfg: Cfg,
|
pub cfg: Cfg,
|
||||||
db: PgPool,
|
db: PgPool,
|
||||||
cache: MultiplexedConnection,
|
cache: MultiplexedConnection,
|
||||||
|
hub: Addr<LiveHub>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ctx {
|
impl Ctx {
|
||||||
pub async fn new(cfg: Cfg) -> Result<Self, Error> {
|
pub async fn new(cfg: Cfg) -> Result<Self, Error> {
|
||||||
let db = PgPool::connect(&cfg.server.database_url).await?;
|
let db = PgPool::connect(&cfg.server.database_url).await?;
|
||||||
let cache = Client::open(cfg.server.cache_url.as_str())?
|
let client = Client::open(cfg.server.cache_url.as_str())?;
|
||||||
.get_multiplexed_async_connection()
|
let cache = client.get_multiplexed_async_connection().await?;
|
||||||
.await?;
|
let sync_cache = client.get_connection()?;
|
||||||
|
let hub = LiveHub::new(sync_cache).start();
|
||||||
|
|
||||||
Ok(Self { cfg, db, cache })
|
Ok(Self {
|
||||||
|
cfg,
|
||||||
|
db,
|
||||||
|
cache,
|
||||||
|
hub,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bind_addr(&self) -> SocketAddr {
|
pub fn bind_addr(&self) -> SocketAddr {
|
||||||
@ -33,4 +41,8 @@ impl Ctx {
|
|||||||
pub fn cache(&self) -> MultiplexedConnection {
|
pub fn cache(&self) -> MultiplexedConnection {
|
||||||
self.cache.clone()
|
self.cache.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn hub(&self) -> Addr<LiveHub> {
|
||||||
|
self.hub.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use redis::{cmd, AsyncCommands, JsonAsyncCommands};
|
use redis::{cmd, AsyncCommands, Connection, JsonAsyncCommands, JsonCommands};
|
||||||
use sqlx::{query, query_as, types::Json};
|
use sqlx::{query, query_as, types::Json};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -86,6 +86,17 @@ impl Board {
|
|||||||
Ok(board)
|
Ok(board)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn read_sync(cache: &mut Connection, id: String) -> Result<Option<Self>, NekrochanError> {
|
||||||
|
let board: Option<String> = cache.json_get(format!("boards:{id}"), ".")?;
|
||||||
|
|
||||||
|
let board = match board {
|
||||||
|
Some(json) => Some(serde_json::from_str(&json)?),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(board)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
|
||||||
let mut boards = Vec::new();
|
let mut boards = Vec::new();
|
||||||
let ids: Vec<String> = ctx.cache().lrange("board_ids", 0, -1).await?;
|
let ids: Vec<String> = ctx.cache().lrange("board_ids", 0, -1).await?;
|
||||||
|
@ -16,7 +16,7 @@ pub struct Account {
|
|||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize)]
|
#[derive(FromRow, Serialize, Deserialize, Clone)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -25,7 +25,7 @@ pub struct Board {
|
|||||||
pub created: DateTime<Utc>,
|
pub created: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
#[derive(FromRow, Serialize, Deserialize, Clone)]
|
||||||
pub struct Ban {
|
pub struct Ban {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub ip_range: IpNetwork,
|
pub ip_range: IpNetwork,
|
||||||
@ -45,7 +45,7 @@ pub struct Report {
|
|||||||
pub reporter_ip: IpAddr,
|
pub reporter_ip: IpAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromRow, Serialize, Deserialize)]
|
#[derive(FromRow, Serialize, Deserialize, Clone)]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: i64,
|
pub id: i64,
|
||||||
pub board: String,
|
pub board: String,
|
||||||
|
@ -5,7 +5,12 @@ use sqlx::{query, query_as, types::Json};
|
|||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
|
|
||||||
use super::models::{Board, File, Post, Report};
|
use super::models::{Board, File, Post, Report};
|
||||||
use crate::{ctx::Ctx, error::NekrochanError, GENERIC_PAGE_SIZE};
|
use crate::{
|
||||||
|
ctx::Ctx,
|
||||||
|
error::NekrochanError,
|
||||||
|
live_hub::{PostCreatedMessage, PostRemovedMessage, PostUpdatedMessage},
|
||||||
|
GENERIC_PAGE_SIZE,
|
||||||
|
};
|
||||||
|
|
||||||
impl Post {
|
impl Post {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@ -23,7 +28,7 @@ impl Post {
|
|||||||
password: String,
|
password: String,
|
||||||
country: String,
|
country: String,
|
||||||
ip: IpAddr,
|
ip: IpAddr,
|
||||||
bumpy_bump: bool,
|
bump: bool,
|
||||||
) -> Result<Self, NekrochanError> {
|
) -> Result<Self, NekrochanError> {
|
||||||
let post: Post = query_as(&format!(
|
let post: Post = query_as(&format!(
|
||||||
r#"INSERT INTO posts_{}
|
r#"INSERT INTO posts_{}
|
||||||
@ -54,7 +59,7 @@ impl Post {
|
|||||||
.execute(ctx.db())
|
.execute(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if bumpy_bump {
|
if bump {
|
||||||
query(&format!(
|
query(&format!(
|
||||||
"UPDATE posts_{} SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE id = $1",
|
"UPDATE posts_{} SET bumps = bumps + 1, bumped = CURRENT_TIMESTAMP WHERE id = $1",
|
||||||
board.id
|
board.id
|
||||||
@ -230,6 +235,23 @@ impl Post {
|
|||||||
Ok(replies)
|
Ok(replies)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn read_replies_after(
|
||||||
|
&self,
|
||||||
|
ctx: &Ctx,
|
||||||
|
last: i64,
|
||||||
|
) -> Result<Vec<Self>, NekrochanError> {
|
||||||
|
let replies = query_as(&format!(
|
||||||
|
"SELECT * FROM posts_{} WHERE thread = $1 AND id > $2 ORDER BY created ASC",
|
||||||
|
self.board
|
||||||
|
))
|
||||||
|
.bind(self.id)
|
||||||
|
.bind(last)
|
||||||
|
.fetch_all(ctx.db())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(replies)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn read_all(ctx: &Ctx, board: String) -> Result<Vec<Self>, NekrochanError> {
|
pub async fn read_all(ctx: &Ctx, board: String) -> Result<Vec<Self>, NekrochanError> {
|
||||||
let posts = query_as(&format!("SELECT * FROM posts_{board}"))
|
let posts = query_as(&format!("SELECT * FROM posts_{board}"))
|
||||||
.fetch_all(ctx.db())
|
.fetch_all(ctx.db())
|
||||||
@ -247,39 +269,45 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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!(
|
let post = query_as(&format!(
|
||||||
"UPDATE posts_{} SET user_id = $1 WHERE id = $2",
|
"UPDATE posts_{} SET user_id = $1 WHERE id = $2 RETURNING *",
|
||||||
self.board,
|
self.board,
|
||||||
))
|
))
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(self.id)
|
.bind(self.id)
|
||||||
.execute(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.hub().send(PostCreatedMessage { post }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_sticky(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
pub async fn update_sticky(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||||
query(&format!(
|
let post = query_as(&format!(
|
||||||
"UPDATE posts_{} SET sticky = NOT sticky WHERE id = $1",
|
"UPDATE posts_{} SET sticky = NOT sticky WHERE id = $1 RETURNING *",
|
||||||
self.board
|
self.board
|
||||||
))
|
))
|
||||||
.bind(self.id)
|
.bind(self.id)
|
||||||
.execute(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_lock(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
pub async fn update_lock(&self, ctx: &Ctx) -> Result<(), NekrochanError> {
|
||||||
query(&format!(
|
let post = query_as(&format!(
|
||||||
"UPDATE posts_{} SET locked = NOT locked WHERE id = $1",
|
"UPDATE posts_{} SET locked = NOT locked WHERE id = $1 RETURNING *",
|
||||||
self.board
|
self.board
|
||||||
))
|
))
|
||||||
.bind(self.id)
|
.bind(self.id)
|
||||||
.execute(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,14 +317,14 @@ impl Post {
|
|||||||
content: String,
|
content: String,
|
||||||
content_nomarkup: String,
|
content_nomarkup: String,
|
||||||
) -> Result<(), NekrochanError> {
|
) -> Result<(), NekrochanError> {
|
||||||
query(&format!(
|
let post = query_as(&format!(
|
||||||
"UPDATE posts_{} SET content = $1, content_nomarkup = $2 WHERE id = $3",
|
"UPDATE posts_{} SET content = $1, content_nomarkup = $2 WHERE id = $3 RETURNING *",
|
||||||
self.board
|
self.board
|
||||||
))
|
))
|
||||||
.bind(content)
|
.bind(content)
|
||||||
.bind(&content_nomarkup)
|
.bind(&content_nomarkup)
|
||||||
.bind(self.id)
|
.bind(self.id)
|
||||||
.execute(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let old_key = format!("by_content:{}", digest(self.content_nomarkup.as_bytes()));
|
let old_key = format!("by_content:{}", digest(self.content_nomarkup.as_bytes()));
|
||||||
@ -306,6 +334,7 @@ impl Post {
|
|||||||
|
|
||||||
ctx.cache().zrem(old_key, &member).await?;
|
ctx.cache().zrem(old_key, &member).await?;
|
||||||
ctx.cache().zadd(new_key, &member, score).await?;
|
ctx.cache().zadd(new_key, &member, score).await?;
|
||||||
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@ -317,15 +346,17 @@ impl Post {
|
|||||||
file.spoiler = !file.spoiler;
|
file.spoiler = !file.spoiler;
|
||||||
}
|
}
|
||||||
|
|
||||||
query(&format!(
|
let post = query_as(&format!(
|
||||||
"UPDATE posts_{} SET files = $1 WHERE id = $2",
|
"UPDATE posts_{} SET files = $1 WHERE id = $2 RETURNING *",
|
||||||
self.board
|
self.board
|
||||||
))
|
))
|
||||||
.bind(Json(files))
|
.bind(Json(files))
|
||||||
.bind(self.id)
|
.bind(self.id)
|
||||||
.execute(ctx.db())
|
.fetch_one(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.hub().send(PostUpdatedMessage { post }).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,6 +396,9 @@ impl Post {
|
|||||||
|
|
||||||
ctx.cache().zrem(ip_key, &member).await?;
|
ctx.cache().zrem(ip_key, &member).await?;
|
||||||
ctx.cache().zrem(content_key, &member).await?;
|
ctx.cache().zrem(content_key, &member).await?;
|
||||||
|
ctx.hub()
|
||||||
|
.send(PostRemovedMessage { post: post.clone() })
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let in_list = to_be_deleted
|
let in_list = to_be_deleted
|
||||||
@ -411,6 +445,10 @@ impl Post {
|
|||||||
.execute(ctx.db())
|
.execute(ctx.db())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
ctx.hub()
|
||||||
|
.send(PostUpdatedMessage { post: self.clone() })
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
35
src/error.rs
35
src/error.rs
@ -58,6 +58,8 @@ pub enum NekrochanError {
|
|||||||
InvalidCaptchaError,
|
InvalidCaptchaError,
|
||||||
#[error("Neplatná strana.")]
|
#[error("Neplatná strana.")]
|
||||||
InvalidPageError,
|
InvalidPageError,
|
||||||
|
#[error("Tento příspěvek není vlákno.")]
|
||||||
|
IsReplyError,
|
||||||
#[error("Obsah musí mít 1-20000 znaků.")]
|
#[error("Obsah musí mít 1-20000 znaků.")]
|
||||||
NewsContentFormatError,
|
NewsContentFormatError,
|
||||||
#[error("Titulek musí mít 1-100 znaků.")]
|
#[error("Titulek musí mít 1-100 znaků.")]
|
||||||
@ -86,8 +88,6 @@ pub enum NekrochanError {
|
|||||||
ReplyLimitError,
|
ReplyLimitError,
|
||||||
#[error("Hlášení můsí mít 1-200 znaků.")]
|
#[error("Hlášení můsí mít 1-200 znaků.")]
|
||||||
ReportFormatError,
|
ReportFormatError,
|
||||||
#[error("Nelze vytvořit odpověď na odpověď.")]
|
|
||||||
ReplyReplyError,
|
|
||||||
#[error("Na této nástěnce se musí vyplnit CAPTCHA.")]
|
#[error("Na této nástěnce se musí vyplnit CAPTCHA.")]
|
||||||
RequiredCaptchaError,
|
RequiredCaptchaError,
|
||||||
#[error("Toto vlákno je uzamčené.")]
|
#[error("Toto vlákno je uzamčené.")]
|
||||||
@ -98,10 +98,23 @@ pub enum NekrochanError {
|
|||||||
UsernameFormatError,
|
UsernameFormatError,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<actix::MailboxError> for NekrochanError {
|
||||||
|
fn from(e: actix::MailboxError) -> Self {
|
||||||
|
error!("Internal server error: {e:#?}");
|
||||||
|
Self::InternalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<actix_web::Error> for NekrochanError {
|
||||||
|
fn from(e: actix_web::Error) -> Self {
|
||||||
|
error!("Internal server error: {e:#?}");
|
||||||
|
Self::InternalError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<askama::Error> for NekrochanError {
|
impl From<askama::Error> for NekrochanError {
|
||||||
fn from(e: askama::Error) -> Self {
|
fn from(e: askama::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,7 +122,6 @@ impl From<askama::Error> for NekrochanError {
|
|||||||
impl From<ipnetwork::IpNetworkError> for NekrochanError {
|
impl From<ipnetwork::IpNetworkError> for NekrochanError {
|
||||||
fn from(e: ipnetwork::IpNetworkError) -> Self {
|
fn from(e: ipnetwork::IpNetworkError) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,7 +129,6 @@ impl From<ipnetwork::IpNetworkError> for NekrochanError {
|
|||||||
impl From<jsonwebtoken::errors::Error> for NekrochanError {
|
impl From<jsonwebtoken::errors::Error> for NekrochanError {
|
||||||
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
fn from(e: jsonwebtoken::errors::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -125,7 +136,6 @@ impl From<jsonwebtoken::errors::Error> for NekrochanError {
|
|||||||
impl From<pwhash::error::Error> for NekrochanError {
|
impl From<pwhash::error::Error> for NekrochanError {
|
||||||
fn from(e: pwhash::error::Error) -> Self {
|
fn from(e: pwhash::error::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,7 +143,6 @@ impl From<pwhash::error::Error> for NekrochanError {
|
|||||||
impl From<regex::Error> for NekrochanError {
|
impl From<regex::Error> for NekrochanError {
|
||||||
fn from(e: regex::Error) -> Self {
|
fn from(e: regex::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -141,7 +150,6 @@ impl From<regex::Error> for NekrochanError {
|
|||||||
impl From<redis::RedisError> for NekrochanError {
|
impl From<redis::RedisError> for NekrochanError {
|
||||||
fn from(e: redis::RedisError) -> Self {
|
fn from(e: redis::RedisError) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,7 +157,6 @@ impl From<redis::RedisError> for NekrochanError {
|
|||||||
impl From<serde_json::Error> for NekrochanError {
|
impl From<serde_json::Error> for NekrochanError {
|
||||||
fn from(e: serde_json::Error) -> Self {
|
fn from(e: serde_json::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +164,6 @@ impl From<serde_json::Error> for NekrochanError {
|
|||||||
impl From<serde_qs::Error> for NekrochanError {
|
impl From<serde_qs::Error> for NekrochanError {
|
||||||
fn from(e: serde_qs::Error) -> Self {
|
fn from(e: serde_qs::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,7 +179,6 @@ impl From<sqlx::Error> for NekrochanError {
|
|||||||
Self::OverboardError
|
Self::OverboardError
|
||||||
} else {
|
} else {
|
||||||
error!("{e:#?}");
|
error!("{e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -182,7 +187,6 @@ impl From<sqlx::Error> for NekrochanError {
|
|||||||
impl From<std::io::Error> for NekrochanError {
|
impl From<std::io::Error> for NekrochanError {
|
||||||
fn from(e: std::io::Error) -> Self {
|
fn from(e: std::io::Error) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,15 +194,13 @@ impl From<std::io::Error> for NekrochanError {
|
|||||||
impl From<std::net::AddrParseError> for NekrochanError {
|
impl From<std::net::AddrParseError> for NekrochanError {
|
||||||
fn from(e: std::net::AddrParseError) -> Self {
|
fn from(e: std::net::AddrParseError) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<std::sync::PoisonError<T>> for NekrochanError {
|
impl<T> From<std::sync::PoisonError<T>> for NekrochanError {
|
||||||
fn from(_: std::sync::PoisonError<T>) -> Self {
|
fn from(_: std::sync::PoisonError<T>) -> Self {
|
||||||
error!("CAPTCHA RwLock got poisoned or something.");
|
error!("Some Mutex or Lock got poisoned or something");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,7 +208,6 @@ impl<T> From<std::sync::PoisonError<T>> for NekrochanError {
|
|||||||
impl From<tokio::task::JoinError> for NekrochanError {
|
impl From<tokio::task::JoinError> for NekrochanError {
|
||||||
fn from(e: tokio::task::JoinError) -> Self {
|
fn from(e: tokio::task::JoinError) -> Self {
|
||||||
error!("Internal server error: {e:#?}");
|
error!("Internal server error: {e:#?}");
|
||||||
|
|
||||||
Self::InternalError
|
Self::InternalError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -241,6 +242,7 @@ impl ResponseError for NekrochanError {
|
|||||||
NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED,
|
NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED,
|
||||||
NekrochanError::InvalidCaptchaError => StatusCode::BAD_REQUEST,
|
NekrochanError::InvalidCaptchaError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST,
|
NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST,
|
||||||
|
NekrochanError::IsReplyError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::NewsContentFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::NewsContentFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::NewsTitleFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::NewsTitleFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::NoCaptchaError => StatusCode::NOT_FOUND,
|
NekrochanError::NoCaptchaError => StatusCode::NOT_FOUND,
|
||||||
@ -254,7 +256,6 @@ impl ResponseError for NekrochanError {
|
|||||||
NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::PostNameFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND,
|
NekrochanError::PostNotFound(_, _) => StatusCode::NOT_FOUND,
|
||||||
NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN,
|
NekrochanError::ReplyLimitError => StatusCode::FORBIDDEN,
|
||||||
NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST,
|
|
||||||
NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST,
|
NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST,
|
||||||
NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED,
|
NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED,
|
||||||
NekrochanError::ThreadLockError => StatusCode::FORBIDDEN,
|
NekrochanError::ThreadLockError => StatusCode::FORBIDDEN,
|
||||||
|
@ -11,62 +11,6 @@ lazy_static! {
|
|||||||
Regex::new(r#"<a class="quote" href=".+">>>(\d+)<\/a>"#).unwrap();
|
Regex::new(r#"<a class="quote" href=".+">>>(\d+)<\/a>"#).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn czech_humantime(time: &DateTime<Utc>) -> askama::Result<String> {
|
|
||||||
let duration = (Utc::now() - *time).abs();
|
|
||||||
|
|
||||||
let seconds = duration.num_seconds();
|
|
||||||
let minutes = duration.num_minutes();
|
|
||||||
let hours = duration.num_hours();
|
|
||||||
let days = duration.num_days();
|
|
||||||
let weeks = duration.num_weeks();
|
|
||||||
let months = duration.num_days() / 30;
|
|
||||||
let years = duration.num_days() / 365;
|
|
||||||
|
|
||||||
let mut time = "Teď".into();
|
|
||||||
|
|
||||||
if seconds > 0 {
|
|
||||||
time = format!(
|
|
||||||
"{} {}",
|
|
||||||
seconds,
|
|
||||||
czech_plural("sekunda|sekundy|sekund", seconds)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if minutes > 0 {
|
|
||||||
time = format!(
|
|
||||||
"{} {}",
|
|
||||||
minutes,
|
|
||||||
czech_plural("minuta|minuty|minut", minutes)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if hours > 0 {
|
|
||||||
time = format!("{} {}", hours, czech_plural("hodina|hodiny|hodin", hours)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
if days > 0 {
|
|
||||||
time = format!("{} {}", days, czech_plural("den|dny|dnů", days)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
if weeks > 0 {
|
|
||||||
time = format!("{} {}", weeks, czech_plural("týden|týdny|týdnů", weeks)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
if months > 0 {
|
|
||||||
time = format!(
|
|
||||||
"{} {}",
|
|
||||||
months,
|
|
||||||
czech_plural("měsíc|měsíce|měsíců", months)?
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if years > 0 {
|
|
||||||
time = format!("{} {}", years, czech_plural("rok|roky|let", years)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(time)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn czech_datetime(utc: &DateTime<Utc>) -> askama::Result<String> {
|
pub fn czech_datetime(utc: &DateTime<Utc>) -> askama::Result<String> {
|
||||||
let time = Prague.from_utc_datetime(&utc.naive_utc());
|
let time = Prague.from_utc_datetime(&utc.naive_utc());
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ pub mod db;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
pub mod filters;
|
pub mod filters;
|
||||||
|
pub mod live_hub;
|
||||||
|
pub mod live_session;
|
||||||
pub mod markup;
|
pub mod markup;
|
||||||
pub mod perms;
|
pub mod perms;
|
||||||
pub mod qsform;
|
pub mod qsform;
|
||||||
@ -20,9 +22,9 @@ pub fn paginate(page_size: i64, count: i64) -> i64 {
|
|||||||
let pages = count / page_size + (count % page_size).signum();
|
let pages = count / page_size + (count % page_size).signum();
|
||||||
|
|
||||||
if pages == 0 {
|
if pages == 0 {
|
||||||
return 1;
|
1
|
||||||
} else {
|
} else {
|
||||||
return pages;
|
pages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
262
src/live_hub.rs
Normální soubor
262
src/live_hub.rs
Normální soubor
@ -0,0 +1,262 @@
|
|||||||
|
use actix::{Actor, Context, Handler, Message, Recipient};
|
||||||
|
use askama::Template;
|
||||||
|
use redis::Connection;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::models::{Board, Post},
|
||||||
|
filters,
|
||||||
|
web::tcx::TemplateCtx,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct SessionMessage(pub String);
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct ConnectMessage {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub thread: (String, i64),
|
||||||
|
pub tcx: TemplateCtx,
|
||||||
|
pub recv: Recipient<SessionMessage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct DisconnectMessage {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub thread: (String, i64),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct PostCreatedMessage {
|
||||||
|
pub post: Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct TargetedPostCreatedMessage {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub post: Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct PostUpdatedMessage {
|
||||||
|
pub post: Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "()")]
|
||||||
|
pub struct PostRemovedMessage {
|
||||||
|
pub post: Post,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(
|
||||||
|
ext = "html",
|
||||||
|
source = "{% import \"./macros/post.html\" as post %}{% call post::post(board, post, post.thread.is_some()) %}"
|
||||||
|
)]
|
||||||
|
struct PostTemplate {
|
||||||
|
tcx: TemplateCtx,
|
||||||
|
post: Post,
|
||||||
|
board: Board,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LiveHub {
|
||||||
|
pub cache: Connection,
|
||||||
|
pub recv_by_uuid: HashMap<Uuid, (TemplateCtx, Recipient<SessionMessage>)>,
|
||||||
|
pub recv_by_thread: HashMap<(String, i64), Vec<Uuid>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LiveHub {
|
||||||
|
pub fn new(cache: Connection) -> Self {
|
||||||
|
Self {
|
||||||
|
cache,
|
||||||
|
recv_by_uuid: HashMap::new(),
|
||||||
|
recv_by_thread: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for LiveHub {
|
||||||
|
type Context = Context<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<ConnectMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: ConnectMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
self.recv_by_uuid.insert(msg.uuid, (msg.tcx, msg.recv));
|
||||||
|
|
||||||
|
match self.recv_by_thread.get_mut(&msg.thread) {
|
||||||
|
Some(vec) => {
|
||||||
|
vec.push(msg.uuid);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.recv_by_thread.insert(msg.thread, vec![msg.uuid]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<DisconnectMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: DisconnectMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
self.recv_by_uuid.remove(&msg.uuid);
|
||||||
|
|
||||||
|
let recv_by_thread = match self.recv_by_thread.get_mut(&msg.thread) {
|
||||||
|
Some(recv_by_thread) => recv_by_thread,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
*recv_by_thread = recv_by_thread
|
||||||
|
.iter()
|
||||||
|
.filter(|uuid| **uuid != msg.uuid)
|
||||||
|
.map(Uuid::clone)
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<PostCreatedMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: PostCreatedMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
let post = msg.post;
|
||||||
|
|
||||||
|
let uuids = self
|
||||||
|
.recv_by_thread
|
||||||
|
.get(&(post.board.clone(), post.thread.unwrap_or(post.id)));
|
||||||
|
|
||||||
|
let uuids = match uuids {
|
||||||
|
Some(uuids) => uuids,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
let Some((tcx, recv)) = self.recv_by_uuid.get_mut(uuid) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
tcx.update_yous(&mut self.cache).ok();
|
||||||
|
|
||||||
|
let post = post.clone();
|
||||||
|
let id = post.id;
|
||||||
|
let tcx = tcx.clone();
|
||||||
|
let board = board.clone();
|
||||||
|
|
||||||
|
let html = PostTemplate { tcx, post, board }
|
||||||
|
.render()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
recv.do_send(SessionMessage(
|
||||||
|
json!({ "type": "created", "id": id, "html": html }).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<TargetedPostCreatedMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: TargetedPostCreatedMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
let post = msg.post;
|
||||||
|
|
||||||
|
let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some((tcx, recv)) = self.recv_by_uuid.get(&msg.uuid)else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = post.id;
|
||||||
|
let tcx = tcx.clone();
|
||||||
|
|
||||||
|
let html = PostTemplate { tcx, post, board }
|
||||||
|
.render()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
recv.do_send(SessionMessage(
|
||||||
|
json!({ "type": "created", "id": id, "html": html }).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<PostUpdatedMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: PostUpdatedMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
let post = msg.post;
|
||||||
|
|
||||||
|
let uuids = self
|
||||||
|
.recv_by_thread
|
||||||
|
.get(&(post.board.clone(), post.thread.unwrap_or(post.id)));
|
||||||
|
|
||||||
|
let uuids = match uuids {
|
||||||
|
Some(uuids) => uuids,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(Some(board)) = Board::read_sync(&mut self.cache, post.board.clone()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
let Some((tcx, recv)) = self.recv_by_uuid.get_mut(uuid) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
tcx.update_yous(&mut self.cache).ok();
|
||||||
|
|
||||||
|
let post = post.clone();
|
||||||
|
let id = post.id;
|
||||||
|
let tcx = tcx.clone();
|
||||||
|
let board = board.clone();
|
||||||
|
|
||||||
|
let html = PostTemplate { tcx, post, board }
|
||||||
|
.render()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
recv.do_send(SessionMessage(
|
||||||
|
json!({ "type": "updated", "id": id, "html": html }).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<PostRemovedMessage> for LiveHub {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: PostRemovedMessage, _: &mut Self::Context) -> Self::Result {
|
||||||
|
let post = msg.post;
|
||||||
|
|
||||||
|
let uuids = self
|
||||||
|
.recv_by_thread
|
||||||
|
.get(&(post.board.clone(), post.thread.unwrap_or(post.id)));
|
||||||
|
|
||||||
|
let uuids = match uuids {
|
||||||
|
Some(uuids) => uuids,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
for uuid in uuids {
|
||||||
|
let Some((_, recv)) = self.recv_by_uuid.get(uuid) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
recv.do_send(SessionMessage(
|
||||||
|
json!({ "type": "removed", "id": post.id }).to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
src/live_session.rs
Normální soubor
65
src/live_session.rs
Normální soubor
@ -0,0 +1,65 @@
|
|||||||
|
use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, StreamHandler};
|
||||||
|
use actix_web_actors::ws::{Message as WsMessage, ProtocolError, WebsocketContext};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
live_hub::{ConnectMessage, DisconnectMessage, LiveHub, SessionMessage},
|
||||||
|
web::tcx::TemplateCtx,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct LiveSession {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub thread: (String, i64),
|
||||||
|
pub tcx: TemplateCtx,
|
||||||
|
pub hub: Addr<LiveHub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for LiveSession {
|
||||||
|
type Context = WebsocketContext<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<SessionMessage> for LiveSession {
|
||||||
|
type Result = ();
|
||||||
|
|
||||||
|
fn handle(
|
||||||
|
&mut self,
|
||||||
|
SessionMessage(msg): SessionMessage,
|
||||||
|
ctx: &mut Self::Context,
|
||||||
|
) -> Self::Result {
|
||||||
|
ctx.text(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StreamHandler<Result<WsMessage, ProtocolError>> for LiveSession {
|
||||||
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
let uuid = self.uuid;
|
||||||
|
let thread = self.thread.clone();
|
||||||
|
let tcx = self.tcx.clone();
|
||||||
|
let recv = ctx.address().recipient();
|
||||||
|
|
||||||
|
self.hub.do_send(ConnectMessage {
|
||||||
|
uuid,
|
||||||
|
thread,
|
||||||
|
tcx,
|
||||||
|
recv,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: Result<WsMessage, ProtocolError>, ctx: &mut Self::Context) {
|
||||||
|
match msg {
|
||||||
|
Ok(WsMessage::Ping(data)) => ctx.pong(&data),
|
||||||
|
Ok(WsMessage::Close(_)) => self.finished(ctx),
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finished(&mut self, ctx: &mut Self::Context) {
|
||||||
|
self.hub.do_send(DisconnectMessage {
|
||||||
|
uuid: self.uuid,
|
||||||
|
thread: self.thread.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.close(None);
|
||||||
|
ctx.stop();
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ use sqlx::migrate;
|
|||||||
use std::{env::var, time::Duration};
|
use std::{env::var, time::Duration};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
#[tokio::main]
|
#[actix_web::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenv::dotenv().ok();
|
dotenv::dotenv().ok();
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@ -66,6 +66,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
.service(web::captcha::captcha)
|
.service(web::captcha::captcha)
|
||||||
.service(web::edit_posts::edit_posts)
|
.service(web::edit_posts::edit_posts)
|
||||||
.service(web::ip_posts::ip_posts)
|
.service(web::ip_posts::ip_posts)
|
||||||
|
.service(web::live::live)
|
||||||
.service(web::login::login_get)
|
.service(web::login::login_get)
|
||||||
.service(web::login::login_post)
|
.service(web::login::login_post)
|
||||||
.service(web::logout::logout)
|
.service(web::logout::logout)
|
||||||
|
@ -23,6 +23,7 @@ pub enum Permissions {
|
|||||||
BypassAntispam,
|
BypassAntispam,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct PermissionWrapper(BitFlags<Permissions>, bool);
|
pub struct PermissionWrapper(BitFlags<Permissions>, bool);
|
||||||
|
|
||||||
impl PermissionWrapper {
|
impl PermissionWrapper {
|
||||||
|
@ -64,7 +64,7 @@ pub async fn create_post(
|
|||||||
return Err(NekrochanError::BoardLockError(board.id.clone()));
|
return Err(NekrochanError::BoardLockError(board.id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut bumpy_bump = true;
|
let mut bump = true;
|
||||||
let mut noko = ctx.cfg.site.default_noko;
|
let mut noko = ctx.cfg.site.default_noko;
|
||||||
|
|
||||||
let thread = match form.thread {
|
let thread = match form.thread {
|
||||||
@ -74,7 +74,7 @@ pub async fn create_post(
|
|||||||
.ok_or(NekrochanError::PostNotFound(board.id.clone(), thread))?;
|
.ok_or(NekrochanError::PostNotFound(board.id.clone(), thread))?;
|
||||||
|
|
||||||
if thread.thread.is_some() {
|
if thread.thread.is_some() {
|
||||||
return Err(NekrochanError::ReplyReplyError);
|
return Err(NekrochanError::IsReplyError);
|
||||||
}
|
}
|
||||||
|
|
||||||
if thread.locked && !(perms.owner() || perms.bypass_thread_lock()) {
|
if thread.locked && !(perms.owner() || perms.bypass_thread_lock()) {
|
||||||
@ -86,7 +86,7 @@ pub async fn create_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if thread.bumps >= board.config.0.bump_limit {
|
if thread.bumps >= board.config.0.bump_limit {
|
||||||
bumpy_bump = false;
|
bump = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(thread)
|
Some(thread)
|
||||||
@ -137,16 +137,32 @@ pub async fn create_post(
|
|||||||
|
|
||||||
let email_lower = email_raw.to_lowercase();
|
let email_lower = email_raw.to_lowercase();
|
||||||
|
|
||||||
if email_lower.contains("sage") {
|
if email_lower == "sage" {
|
||||||
bumpy_bump = false;
|
bump = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.cfg.site.default_noko && email_lower.contains("noko") {
|
if !ctx.cfg.site.default_noko && email_lower == "noko" {
|
||||||
noko = true
|
noko = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.cfg.site.default_noko && email_lower.contains("nonoko") {
|
if ctx.cfg.site.default_noko {
|
||||||
noko = false
|
if email_lower == "nonoko" {
|
||||||
|
noko = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if email_lower == "nonokosage" {
|
||||||
|
noko = false;
|
||||||
|
bump = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if email_lower == "noko" {
|
||||||
|
noko = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if email_lower == "nokosage" {
|
||||||
|
noko = true;
|
||||||
|
bump = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(email_raw.into())
|
Some(email_raw.into())
|
||||||
@ -221,7 +237,7 @@ pub async fn create_post(
|
|||||||
password,
|
password,
|
||||||
country,
|
country,
|
||||||
ip,
|
ip,
|
||||||
bumpy_bump,
|
bump,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ pub async fn get_posts_from_ids(ctx: &Ctx, ids: &Vec<String>) -> Vec<Post> {
|
|||||||
let mut posts = Vec::new();
|
let mut posts = Vec::new();
|
||||||
|
|
||||||
for id in ids {
|
for id in ids {
|
||||||
if let Some((board, id)) = parse_id(&id) {
|
if let Some((board, id)) = parse_id(id) {
|
||||||
if let Ok(Some(post)) = Post::read(ctx, board, id).await {
|
if let Ok(Some(post)) = Post::read(ctx, board, id).await {
|
||||||
posts.push(post);
|
posts.push(post);
|
||||||
}
|
}
|
||||||
|
@ -125,8 +125,12 @@ pub async fn staff_post_actions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if form.toggle_lock.is_some() {
|
if form.toggle_lock.is_some() {
|
||||||
post.update_lock(&ctx).await?;
|
if post.thread.is_some() {
|
||||||
locks_toggled += 1;
|
writeln!(&mut response, "[Chyba] Odpověď nelze uzamknout.").ok();
|
||||||
|
} else {
|
||||||
|
post.update_lock(&ctx).await?;
|
||||||
|
locks_toggled += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,15 +149,10 @@ pub async fn staff_post_actions(
|
|||||||
let mut already_banned = HashSet::new();
|
let mut already_banned = HashSet::new();
|
||||||
|
|
||||||
for post in &posts {
|
for post in &posts {
|
||||||
if let (
|
if let ((Some(_), _) | (_, Some(_)), reason, duration, Some(range)) = (
|
||||||
(Some(_), None) | (None, Some(_)) | (Some(_), Some(_)),
|
|
||||||
Some(reason),
|
|
||||||
Some(duration),
|
|
||||||
Some(range),
|
|
||||||
) = (
|
|
||||||
(form.ban_user.clone(), form.ban_reporters.clone()),
|
(form.ban_user.clone(), form.ban_reporters.clone()),
|
||||||
form.ban_reason.clone(),
|
form.ban_reason.clone().unwrap_or_default(),
|
||||||
form.ban_duration,
|
form.ban_duration.unwrap_or_default(),
|
||||||
form.ban_range.clone(),
|
form.ban_range.clone(),
|
||||||
) {
|
) {
|
||||||
if !(account.perms().owner() || account.perms().bans()) {
|
if !(account.perms().owner() || account.perms().bans()) {
|
||||||
@ -222,19 +221,24 @@ pub async fn staff_post_actions(
|
|||||||
for post in &posts {
|
for post in &posts {
|
||||||
if form.troll_user.is_some() {
|
if form.troll_user.is_some() {
|
||||||
if !(tcx.perms.owner() || tcx.perms.edit_posts()) {
|
if !(tcx.perms.owner() || tcx.perms.edit_posts()) {
|
||||||
writeln!(&mut response, "[Chyba] Nemáš oprávnění upravovat příspěvky.").ok();
|
writeln!(
|
||||||
|
&mut response,
|
||||||
|
"[Chyba] Nemáš oprávnění upravovat příspěvky."
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(tcx.perms.owner() || tcx.perms.view_ips()) {
|
if !(tcx.perms.owner() || tcx.perms.view_ips()) {
|
||||||
writeln!(&mut response, "[Chyba] Nemáš oprávnění zobrazovat IP adresy.").ok();
|
writeln!(
|
||||||
|
&mut response,
|
||||||
|
"[Chyba] Nemáš oprávnění zobrazovat IP adresy."
|
||||||
|
)
|
||||||
|
.ok();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_nomarkup = format!(
|
let content_nomarkup = format!("{}\n\n##({})##", post.content_nomarkup, post.ip);
|
||||||
"{}\n\n##({})##",
|
|
||||||
post.content_nomarkup, post.ip
|
|
||||||
);
|
|
||||||
|
|
||||||
let content = format!(
|
let content = format!(
|
||||||
"{}\n\n<span class=\"jannytext\">({})</span>",
|
"{}\n\n<span class=\"jannytext\">({})</span>",
|
||||||
@ -246,7 +250,6 @@ pub async fn staff_post_actions(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if posts_removed != 0 {
|
if posts_removed != 0 {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut response,
|
&mut response,
|
||||||
@ -312,6 +315,7 @@ pub async fn staff_post_actions(
|
|||||||
template_response(&template)
|
template_response(&template)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn ban_ip(
|
async fn ban_ip(
|
||||||
ctx: &Ctx,
|
ctx: &Ctx,
|
||||||
account: &Account,
|
account: &Account,
|
||||||
@ -359,7 +363,7 @@ async fn ban_ip(
|
|||||||
Some(Utc::now() + Duration::days(duration as i64))
|
Some(Utc::now() + Duration::days(duration as i64))
|
||||||
};
|
};
|
||||||
|
|
||||||
Ban::create(&ctx, account, board, ip_range, reason, appealable, expires).await?;
|
Ban::create(ctx, account, board, ip_range, reason, appealable, expires).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
62
src/web/live.rs
Normální soubor
62
src/web/live.rs
Normální soubor
@ -0,0 +1,62 @@
|
|||||||
|
use actix_web::{
|
||||||
|
get,
|
||||||
|
web::{Data, Path, Payload},
|
||||||
|
HttpRequest, HttpResponse,
|
||||||
|
};
|
||||||
|
use actix_web_actors::ws;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ctx::Ctx,
|
||||||
|
db::models::{Board, Post},
|
||||||
|
error::NekrochanError,
|
||||||
|
live_hub::TargetedPostCreatedMessage,
|
||||||
|
live_session::LiveSession,
|
||||||
|
web::tcx::TemplateCtx,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[get("/live/{board}/{id}/{last}")]
|
||||||
|
pub async fn live(
|
||||||
|
ctx: Data<Ctx>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: Path<(String, i64, i64)>,
|
||||||
|
stream: Payload,
|
||||||
|
) -> Result<HttpResponse, NekrochanError> {
|
||||||
|
let (board, id, last) = path.into_inner();
|
||||||
|
|
||||||
|
let board = Board::read(&ctx, board.clone())
|
||||||
|
.await?
|
||||||
|
.ok_or(NekrochanError::BoardNotFound(board))?;
|
||||||
|
|
||||||
|
let post = Post::read(&ctx, board.id.clone(), id)
|
||||||
|
.await?
|
||||||
|
.ok_or(NekrochanError::PostNotFound(board.id.clone(), id))?;
|
||||||
|
|
||||||
|
if post.thread.is_some() {
|
||||||
|
return Err(NekrochanError::IsReplyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
let thread = (board.id, id);
|
||||||
|
let tcx = TemplateCtx::new(&ctx, &req).await?;
|
||||||
|
let hub = ctx.hub();
|
||||||
|
|
||||||
|
let ws = LiveSession {
|
||||||
|
uuid,
|
||||||
|
thread,
|
||||||
|
tcx,
|
||||||
|
hub,
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = ws::start(ws, &req, stream)?;
|
||||||
|
|
||||||
|
let new_replies = post.read_replies_after(&ctx, last).await?;
|
||||||
|
|
||||||
|
for post in new_replies {
|
||||||
|
ctx.hub()
|
||||||
|
.send(TargetedPostCreatedMessage { uuid, post })
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
@ -5,6 +5,7 @@ pub mod captcha;
|
|||||||
pub mod edit_posts;
|
pub mod edit_posts;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod ip_posts;
|
pub mod ip_posts;
|
||||||
|
pub mod live;
|
||||||
pub mod login;
|
pub mod login;
|
||||||
pub mod logout;
|
pub mod logout;
|
||||||
pub mod news;
|
pub mod news;
|
||||||
|
@ -9,7 +9,13 @@ use serde::Deserialize;
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
check_page, ctx::Ctx, db::models::{Board, Post}, error::NekrochanError, filters, paginate, web::{tcx::TemplateCtx, template_response}, GENERIC_PAGE_SIZE
|
check_page,
|
||||||
|
ctx::Ctx,
|
||||||
|
db::models::{Board, Post},
|
||||||
|
error::NekrochanError,
|
||||||
|
filters, paginate,
|
||||||
|
web::{tcx::TemplateCtx, template_response},
|
||||||
|
GENERIC_PAGE_SIZE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -5,6 +5,7 @@ use crate::{
|
|||||||
ctx::Ctx,
|
ctx::Ctx,
|
||||||
db::models::NewsPost,
|
db::models::NewsPost,
|
||||||
error::NekrochanError,
|
error::NekrochanError,
|
||||||
|
filters,
|
||||||
web::{tcx::TemplateCtx, template_response},
|
web::{tcx::TemplateCtx, template_response},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use actix_web::HttpRequest;
|
use actix_web::HttpRequest;
|
||||||
use redis::AsyncCommands;
|
use redis::{AsyncCommands, Commands, Connection};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
net::{IpAddr, Ipv4Addr},
|
net::{IpAddr, Ipv4Addr},
|
||||||
@ -10,12 +10,12 @@ use crate::{
|
|||||||
perms::PermissionWrapper,
|
perms::PermissionWrapper,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct TemplateCtx {
|
pub struct TemplateCtx {
|
||||||
pub cfg: Cfg,
|
pub cfg: Cfg,
|
||||||
pub boards: Vec<String>,
|
pub boards: Vec<String>,
|
||||||
pub account: Option<String>,
|
pub account: Option<String>,
|
||||||
pub perms: PermissionWrapper,
|
pub perms: PermissionWrapper,
|
||||||
pub name: Option<String>,
|
|
||||||
pub ip: IpAddr,
|
pub ip: IpAddr,
|
||||||
pub yous: HashSet<String>,
|
pub yous: HashSet<String>,
|
||||||
}
|
}
|
||||||
@ -32,8 +32,6 @@ impl TemplateCtx {
|
|||||||
None => PermissionWrapper::new(0, false),
|
None => PermissionWrapper::new(0, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = req.cookie("name").map(|cookie| cookie.value().into());
|
|
||||||
|
|
||||||
let (ip, _) = ip_from_req(req)?;
|
let (ip, _) = ip_from_req(req)?;
|
||||||
let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?;
|
let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?;
|
||||||
|
|
||||||
@ -43,7 +41,6 @@ impl TemplateCtx {
|
|||||||
cfg,
|
cfg,
|
||||||
boards,
|
boards,
|
||||||
perms,
|
perms,
|
||||||
name,
|
|
||||||
ip,
|
ip,
|
||||||
yous,
|
yous,
|
||||||
account,
|
account,
|
||||||
@ -51,6 +48,11 @@ impl TemplateCtx {
|
|||||||
|
|
||||||
Ok(tcx)
|
Ok(tcx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_yous(&mut self, cache: &mut Connection) -> Result<(), NekrochanError> {
|
||||||
|
self.yous = cache.zrange(format!("by_ip:{}", self.ip), 0, -1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn account_from_auth(ctx: &Ctx, req: &HttpRequest) -> Result<Account, NekrochanError> {
|
pub async fn account_from_auth(ctx: &Ctx, req: &HttpRequest) -> Result<Account, NekrochanError> {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
|
let name = get_cookie("name");
|
||||||
let password = get_cookie("password");
|
let password = get_cookie("password");
|
||||||
|
|
||||||
if (password === "") {
|
if (password === "") {
|
||||||
@ -6,6 +7,7 @@ $(function () {
|
|||||||
set_cookie("password", password);
|
set_cookie("password", password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$('input[name="name"]').prop("value", name);
|
||||||
$('input[name="password"]').prop("value", password);
|
$('input[name="password"]').prop("value", password);
|
||||||
});
|
});
|
||||||
|
|
@ -1,23 +1,29 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
$(".expandable").click(function () {
|
update_expandable($(".expandable"));
|
||||||
let src_link = $(this).attr("href");
|
|
||||||
|
|
||||||
let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some(
|
|
||||||
(ext) => src_link.endsWith(ext)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!is_video) {
|
|
||||||
toggle_image($(this), src_link);
|
|
||||||
} else {
|
|
||||||
toggle_video($(this), src_link);
|
|
||||||
}
|
|
||||||
|
|
||||||
$(this).toggleClass("expanded");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function update_expandable(elements) {
|
||||||
|
elements.each(function() {
|
||||||
|
$(this).click(function() {
|
||||||
|
let src_link = $(this).attr("href");
|
||||||
|
|
||||||
|
let is_video = [".mpeg", ".mov", ".mp4", ".webm", ".mkv", ".ogg"].some(
|
||||||
|
(ext) => src_link.endsWith(ext)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!is_video) {
|
||||||
|
toggle_image($(this), src_link);
|
||||||
|
} else {
|
||||||
|
toggle_video($(this), src_link);
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).toggleClass("expanded");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function toggle_image(parent, src_link) {
|
function toggle_image(parent, src_link) {
|
||||||
let thumb = parent.find(".thumb");
|
let thumb = parent.find(".thumb");
|
||||||
let src = parent.find(".src");
|
let src = parent.find(".src");
|
64
static/js/live.js
Normální soubor
64
static/js/live.js
Normální soubor
@ -0,0 +1,64 @@
|
|||||||
|
$(function () {
|
||||||
|
let main = $(".thread + hr");
|
||||||
|
|
||||||
|
main.after(
|
||||||
|
'<div id="live-info" class="box inline-block"><span id="live-indicator"></span> <span id="live-status"></span></div><br>'
|
||||||
|
);
|
||||||
|
|
||||||
|
$("#live-indicator").css("background-color", "orange");
|
||||||
|
$("#live-status").text("Připojování...");
|
||||||
|
|
||||||
|
let thread = window.location.pathname.split("/").slice(-2);
|
||||||
|
let protocol;
|
||||||
|
|
||||||
|
if (window.location.protocol === "https:") {
|
||||||
|
protocol = "wss:";
|
||||||
|
} else {
|
||||||
|
protocol = "ws:";
|
||||||
|
}
|
||||||
|
|
||||||
|
let last_post = $(".thread").find(".post").last().attr("id");
|
||||||
|
|
||||||
|
let ws_location = `${protocol}//${window.location.host}/live/${thread[0]}/${thread[1]}/${last_post}`;
|
||||||
|
let ws = new WebSocket(ws_location);
|
||||||
|
|
||||||
|
ws.addEventListener("open", function (_) {
|
||||||
|
$("#live-indicator").css("background-color", "lime");
|
||||||
|
$("#live-status").text("Připojeno pro nové příspěvky");
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("message", function (msg) {
|
||||||
|
let data = JSON.parse(msg.data);
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case "created":
|
||||||
|
$(".thread").append(data.html + "<br>");
|
||||||
|
update_expandable($(`#${data.id} .expandable`));
|
||||||
|
update_reltimes($(`#${data.id}`).find("time"));
|
||||||
|
break;
|
||||||
|
case "updated":
|
||||||
|
$(`#${data.id}`).replaceWith(data.html);
|
||||||
|
update_expandable($(`#${data.id} .expandable`));
|
||||||
|
update_reltimes($(`#${data.id}`).find("time"));
|
||||||
|
break;
|
||||||
|
case "removed":
|
||||||
|
if (data.id === parseInt(thread[1])) {
|
||||||
|
ws.close();
|
||||||
|
$("#live-indicator").css("background-color", "red");
|
||||||
|
$("#live-status").text("Vlákno bylo odstraněno");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(`#${data.id}`).next("br").remove();
|
||||||
|
$(`#${data.id}`).remove();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener("close", function (_) {
|
||||||
|
$("#live-indicator").css("background-color", "red");
|
||||||
|
$("#live-status").text("Odpojeno, obnov stránku");
|
||||||
|
});
|
||||||
|
});
|
112
static/js/time.js
Normální soubor
112
static/js/time.js
Normální soubor
@ -0,0 +1,112 @@
|
|||||||
|
$(function () {
|
||||||
|
update_reltimes($("time"));
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
update_reltimes($("time"));
|
||||||
|
}, 60000);
|
||||||
|
});
|
||||||
|
|
||||||
|
function update_reltimes(elements) {
|
||||||
|
elements.each(function () {
|
||||||
|
let title = $(this).attr("title");
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
$(this).prop("title", $(this).text());
|
||||||
|
}
|
||||||
|
|
||||||
|
let rel = reltime($(this).attr("datetime"));
|
||||||
|
|
||||||
|
$(this).text(rel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const MINUTE = 60000,
|
||||||
|
HOUR = 3600000,
|
||||||
|
DAY = 86400000,
|
||||||
|
WEEK = 604800000,
|
||||||
|
MONTH = 2592000000,
|
||||||
|
YEAR = 31536000000;
|
||||||
|
|
||||||
|
function reltime(date) {
|
||||||
|
let delta = Date.now() - Date.parse(date);
|
||||||
|
let fut = false;
|
||||||
|
|
||||||
|
if (delta < 0) {
|
||||||
|
delta = Math.abs(delta);
|
||||||
|
fut = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let minutes = Math.floor(delta / MINUTE);
|
||||||
|
let hours = Math.floor(delta / HOUR);
|
||||||
|
let days = Math.floor(delta / DAY);
|
||||||
|
let weeks = Math.floor(delta / WEEK);
|
||||||
|
let months = Math.floor(delta / MONTH);
|
||||||
|
let years = Math.floor(delta / YEAR);
|
||||||
|
|
||||||
|
let rt = "Teď";
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${minutes} ${plural("minutu|minuty|minut", minutes)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${minutes} ${plural("minutou|minutami|minutami", minutes)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${hours} ${plural("hodinu|hodiny|hodin", hours)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${hours} ${plural("hodinou|hodinami|hodinami", hours)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${days} ${plural("den|dny|dnů", days)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${days} ${plural("dnem|dny|dny", days)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weeks > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${weeks} ${plural("týden|týdny", weeks)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${weeks} ${plural("týdnem|týdny", weeks)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (months > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${months} ${plural("měsíc|měsíce|měsíců", months)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${months} ${plural("měsícem|měsíci|měsíci", months)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (years > 0) {
|
||||||
|
if (fut) {
|
||||||
|
rt = `za ${years} ${plural("rok|roky|let", years)}`;
|
||||||
|
} else {
|
||||||
|
rt = `před ${years} ${plural("rokem|lety|lety", years)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rt;
|
||||||
|
}
|
||||||
|
|
||||||
|
function plural(plurals, count) {
|
||||||
|
let plurals_arr = plurals.split("|");
|
||||||
|
let one = plurals_arr[0];
|
||||||
|
let few = plurals_arr[1];
|
||||||
|
let other = plurals_arr[2];
|
||||||
|
|
||||||
|
if (count === 1) {
|
||||||
|
return one;
|
||||||
|
} else if (count < 5 && count !== 0) {
|
||||||
|
return few;
|
||||||
|
} else {
|
||||||
|
return other;
|
||||||
|
}
|
||||||
|
}
|
@ -18,7 +18,8 @@ a:hover {
|
|||||||
color: var(--link-hover);
|
color: var(--link-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
details,
|
||||||
|
#live-info {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
@ -492,3 +493,11 @@ summary {
|
|||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
margin: 0px auto;
|
margin: 0px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#live-indicator {
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.8em;
|
||||||
|
height: 0.8em;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
<span>
|
<span>
|
||||||
Tvůj ban platí pro IP adresu/rozsah <b>{{ ban.ip_range }}</b> a 
|
Tvůj ban platí pro IP adresu/rozsah <b>{{ ban.ip_range }}</b> a 
|
||||||
{% if let Some(expires) = ban.expires %}
|
{% if let Some(expires) = ban.expires %}
|
||||||
vyprší <b>{{ expires|czech_datetime }} ({{ expires|czech_humantime }}).</b>
|
vyprší <time datetime="{{ expires }}">{{ expires|czech_datetime }}.</time>
|
||||||
{% else %}
|
{% else %}
|
||||||
je <b>trvalý.</b>
|
je <b>trvalý.</b>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -10,8 +10,10 @@
|
|||||||
<link rel="stylesheet" href='/static/themes/{% block theme %}{% include "../theme.txt" %}{% endblock %}'>
|
<link rel="stylesheet" href='/static/themes/{% block theme %}{% include "../theme.txt" %}{% endblock %}'>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<script src="/static/js/jquery.min.js"></script>
|
<script src="/static/js/jquery.min.js"></script>
|
||||||
<script src="/static/js/expand-image.js"></script>
|
<!-- UX scripts -->
|
||||||
<script src="/static/js/password.js"></script>
|
<script src="/static/js/autofill.js"></script>
|
||||||
|
<script src="/static/js/expand.js"></script>
|
||||||
|
<script src="/static/js/time.js"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<h2 class="headline">
|
<h2 class="headline">
|
||||||
<span>{{ news.title }}</span>
|
<span>{{ news.title }}</span>
|
||||||
<span class="float-r">
|
<span class="float-r">
|
||||||
{{ news.author }} - <span title="{{ news.created|czech_humantime }}">{{ news.created|czech_datetime }}</span>
|
{{ news.author }} - <time datetime="{{ news.created }}">{{ news.created|czech_datetime }}</time>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="label">Jméno</td>
|
<td class="label">Jméno</td>
|
||||||
<td>
|
<td>
|
||||||
<input name="name" type="text" placeholder="{{ board.config.0.anon_name }}"{% if let Some(name) = tcx.name %}value="{{ name }}"{% endif %}>
|
<input name="name" type="text" placeholder="{{ board.config.0.anon_name }}">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
{% if board.config.0.flags %}
|
{% if board.config.0.flags %}
|
||||||
<img title="Země: {{ post.country }}" class="icon" src="/static/flags/{{ post.country }}.png"> 
|
<img title="Země: {{ post.country }}" class="icon" src="/static/flags/{{ post.country }}.png"> 
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span title="{{ post.created|czech_datetime }}">{{ post.created|czech_humantime }}</span> 
|
<time datetime="{{ post.created }}">{{ post.created|czech_datetime }}</time> 
|
||||||
{% if board.config.0.user_ids %}
|
{% if board.config.0.user_ids %}
|
||||||
<span class="user-id" style="background-color: #{{ post.user_id }};">{{ post.user_id }}</span> 
|
<span class="user-id" style="background-color: #{{ post.user_id }};">{{ post.user_id }}</span> 
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<h2 class="headline">
|
<h2 class="headline">
|
||||||
<span>{{ newspost.title }}</span>
|
<span>{{ newspost.title }}</span>
|
||||||
<span class="float-r">
|
<span class="float-r">
|
||||||
{{ newspost.author }} - <span title="{{ newspost.created|czech_humantime }}">{{ newspost.created|czech_datetime }}</span>
|
{{ newspost.author }} - <time datetime="{{ newspost.created }}">{{ newspost.created|czech_datetime }}</time>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<td><input name="accounts[]" type="checkbox" value="{{ account.username }}" {% if !tcx.perms.owner() %}disabled=""{% endif %}></td>
|
<td><input name="accounts[]" type="checkbox" value="{{ account.username }}" {% if !tcx.perms.owner() %}disabled=""{% endif %}></td>
|
||||||
<td>{{ account.username }}</td>
|
<td>{{ account.username }}</td>
|
||||||
<td>{% if account.owner %}Ano{% else %}Ne{% endif %}</td>
|
<td>{% if account.owner %}Ano{% else %}Ne{% endif %}</td>
|
||||||
<td title="{{ account.created|czech_humantime }}">{{ account.created|czech_datetime }}</td>
|
<td><time datetime="{{ account.created }}">{{ account.created|czech_datetime }}</time></td>
|
||||||
<td>{{ account.permissions.0 }} <a href="/staff/permissions/{{ account.username }}">[Zobrazit]</a></td>
|
<td>{{ account.permissions.0 }} <a href="/staff/permissions/{{ account.username }}">[Zobrazit]</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -38,9 +38,9 @@
|
|||||||
<td>{{ ban.issued_by }}</td>
|
<td>{{ ban.issued_by }}</td>
|
||||||
<td>{% if ban.appealable %}Ano{% else %}Ne{% endif %}</td>
|
<td>{% if ban.appealable %}Ano{% else %}Ne{% endif %}</td>
|
||||||
<td>{% if let Some(appeal) = ban.appeal %}<div class="post-content">{{ appeal }}</div>{% else %}-{% endif %}</td>
|
<td>{% if let Some(appeal) = ban.appeal %}<div class="post-content">{{ appeal }}</div>{% else %}-{% endif %}</td>
|
||||||
<td title="ban.{{ ban.created|czech_humantime }}">{{ ban.created|czech_datetime }}</td>
|
<td><time datetime="{{ ban.created }}">{{ ban.created|czech_datetime }}</time></td>
|
||||||
{% if let Some(expires) = ban.expires %}
|
{% if let Some(expires) = ban.expires %}
|
||||||
<td title="{{ expires|czech_humantime }}">{{ expires|czech_datetime }}</td>
|
<td><time datetime="{{ expires }}">{{ expires|czech_datetime }}</time></td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td>Nikdy</td>
|
<td>Nikdy</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
<td>/{{ board.id }}/</td>
|
<td>/{{ board.id }}/</td>
|
||||||
<td>{{ board.name }}</td>
|
<td>{{ board.name }}</td>
|
||||||
<td>{{ board.description }}</td>
|
<td>{{ board.description }}</td>
|
||||||
<td title="{{ board.created|czech_humantime }}">{{ board.created|czech_datetime }}</td>
|
<td><time datetime="{{ board.created }}">{{ board.created|czech_datetime }}</time></td>
|
||||||
<td>{% if tcx.perms.owner() || tcx.perms.board_config() %}<a href="/staff/board-config/{{ 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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<h2 class="headline">
|
<h2 class="headline">
|
||||||
<span>{{ newspost.title }}</span>
|
<span>{{ newspost.title }}</span>
|
||||||
<span class="float-r">
|
<span class="float-r">
|
||||||
{{ newspost.author }} - <span title="{{ newspost.created|czech_humantime }}">{{ newspost.created|czech_datetime }}</span>
|
{{ newspost.author }} - <time datetime="{{ newspost.created }}">{{ newspost.created|czech_datetime }}</time>
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<td><input name="news[]" type="checkbox" value="{{ newspost.id }}"></td>
|
<td><input name="news[]" type="checkbox" value="{{ newspost.id }}"></td>
|
||||||
<td>{{ newspost.title }}</td>
|
<td>{{ newspost.title }}</td>
|
||||||
<td>{{ newspost.author }}</td>
|
<td>{{ newspost.author }}</td>
|
||||||
<td>{{ newspost.created }}</td>
|
<td><time datetime="{{ newspost.created }}">{{ newspost.created|czech_datetime }}</time></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
{% block theme %}{{ board.config.0.board_theme }}{% endblock %}
|
{% block theme %}{{ board.config.0.board_theme }}{% endblock %}
|
||||||
{% block title %}/{{ board.id }}/ - {{ thread.content_nomarkup|inline_post }}{% endblock %}
|
{% block title %}/{{ board.id }}/ - {{ thread.content_nomarkup|inline_post }}{% endblock %}
|
||||||
{% block scripts %}<script src="/static/js/captcha.js"></script>{% endblock %}
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="/static/js/captcha.js"></script>
|
||||||
|
<script src="/static/js/live.js"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
Načítá se…
Odkázat v novém úkolu
Zablokovat Uživatele