Víc džavaskriptu

Tento commit je obsažen v:
sneedmaster 2024-02-20 22:09:52 +01:00
rodič dc07e1f650
revize 3ed82b79a9
20 změnil soubory, kde provedl 204 přidání a 142 odebrání

Zobrazit soubor

@ -1,11 +1,9 @@
use captcha::{gen, Difficulty};
use redis::{cmd, AsyncCommands, JsonAsyncCommands}; use redis::{cmd, AsyncCommands, JsonAsyncCommands};
use sha256::digest;
use sqlx::{query, query_as, types::Json}; use sqlx::{query, query_as, types::Json};
use std::collections::HashMap; use std::collections::HashMap;
use super::models::{Board, File}; use super::models::{Board, File};
use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError, CAPTCHA}; use crate::{cfg::BoardCfg, ctx::Ctx, error::NekrochanError};
impl Board { impl Board {
pub async fn create( pub async fn create(
@ -216,52 +214,6 @@ impl Board {
} }
} }
impl Board {
pub fn thread_captcha(&self) -> Option<(String, String)> {
let captcha = match self.config.thread_captcha.as_str() {
"easy" => gen(Difficulty::Easy),
"medium" => gen(Difficulty::Medium),
"hard" => gen(Difficulty::Hard),
_ => return None,
};
let base64 = captcha.as_base64()?;
let board = self.id.clone();
let difficulty = self.config.thread_captcha.clone();
let id = digest(base64.as_bytes());
let key = (board, difficulty, id.clone());
let solution = captcha.chars_as_string();
CAPTCHA.write().ok()?.insert(key, solution);
Some((id, base64))
}
pub fn reply_captcha(&self) -> Option<(String, String)> {
let captcha = match self.config.reply_captcha.as_str() {
"easy" => gen(Difficulty::Easy),
"medium" => gen(Difficulty::Medium),
"hard" => gen(Difficulty::Hard),
_ => return None,
};
let base64 = captcha.as_base64()?;
let board = self.id.clone();
let difficulty = self.config.thread_captcha.clone();
let id = digest(base64.as_bytes());
let key = (board, difficulty, id.clone());
let solution = captcha.chars_as_string();
CAPTCHA.write().ok()?.insert(key, solution);
Some((id, base64))
}
}
async fn update_overboard(ctx: &Ctx, boards: Vec<Board>) -> Result<(), NekrochanError> { async fn update_overboard(ctx: &Ctx, boards: Vec<Board>) -> Result<(), NekrochanError> {
query("DROP VIEW IF EXISTS overboard") query("DROP VIEW IF EXISTS overboard")
.execute(ctx.db()) .execute(ctx.db())

Zobrazit soubor

@ -54,12 +54,16 @@ pub enum NekrochanError {
InternalError, InternalError,
#[error("Neplatný autentizační token. Vymaž soubory cookie.")] #[error("Neplatný autentizační token. Vymaž soubory cookie.")]
InvalidAuthError, InvalidAuthError,
#[error("Tato CAPTCHA vypršela nebo neexistuje.")]
InvalidCaptchaError,
#[error("Neplatná strana.")] #[error("Neplatná strana.")]
InvalidPageError, InvalidPageError,
#[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ů.")]
NewsTitleFormatError, NewsTitleFormatError,
#[error("Tato nástěnka nevyžaduje CAPTCHA.")]
NoCaptchaError,
#[error("Příspěvek musí mít obsah.")] #[error("Příspěvek musí mít obsah.")]
NoContentError, NoContentError,
#[error("Příspěvek musí mít soubor.")] #[error("Příspěvek musí mít soubor.")]
@ -86,8 +90,6 @@ pub enum NekrochanError {
ReplyReplyError, 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("Tato CAPTCHA neexistuje nebo už byla vyřešena.")]
SolvedCaptchaError,
#[error("Toto vlákno je uzamčené.")] #[error("Toto vlákno je uzamčené.")]
ThreadLockError, ThreadLockError,
#[error("Tento ban nelze odvolat.")] #[error("Tento ban nelze odvolat.")]
@ -237,9 +239,11 @@ impl ResponseError for NekrochanError {
NekrochanError::InsufficientPermissionError => StatusCode::FORBIDDEN, NekrochanError::InsufficientPermissionError => StatusCode::FORBIDDEN,
NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, NekrochanError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED, NekrochanError::InvalidAuthError => StatusCode::UNAUTHORIZED,
NekrochanError::InvalidCaptchaError => StatusCode::BAD_REQUEST,
NekrochanError::InvalidPageError => StatusCode::BAD_REQUEST, NekrochanError::InvalidPageError => 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::NoContentError => StatusCode::BAD_REQUEST, NekrochanError::NoContentError => StatusCode::BAD_REQUEST,
NekrochanError::NoFileError => StatusCode::BAD_REQUEST, NekrochanError::NoFileError => StatusCode::BAD_REQUEST,
NekrochanError::NoPostsError => StatusCode::BAD_REQUEST, NekrochanError::NoPostsError => StatusCode::BAD_REQUEST,
@ -253,7 +257,6 @@ impl ResponseError for NekrochanError {
NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST, NekrochanError::ReplyReplyError => StatusCode::BAD_REQUEST,
NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST, NekrochanError::ReportFormatError => StatusCode::BAD_REQUEST,
NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED, NekrochanError::RequiredCaptchaError => StatusCode::UNAUTHORIZED,
NekrochanError::SolvedCaptchaError => StatusCode::BAD_REQUEST,
NekrochanError::ThreadLockError => StatusCode::FORBIDDEN, NekrochanError::ThreadLockError => StatusCode::FORBIDDEN,
NekrochanError::UnappealableError => StatusCode::BAD_REQUEST, NekrochanError::UnappealableError => StatusCode::BAD_REQUEST,
NekrochanError::UsernameFormatError => StatusCode::BAD_REQUEST, NekrochanError::UsernameFormatError => StatusCode::BAD_REQUEST,

Zobrazit soubor

@ -1,11 +1,4 @@
use error::NekrochanError; use error::NekrochanError;
use lazy_static::lazy_static;
use std::{collections::HashMap, sync::RwLock};
lazy_static! {
pub static ref CAPTCHA: RwLock<HashMap<(String, String, String), String>> =
RwLock::new(HashMap::new());
}
const GENERIC_PAGE_SIZE: i64 = 15; const GENERIC_PAGE_SIZE: i64 = 15;

Zobrazit soubor

@ -63,8 +63,9 @@ async fn run() -> Result<(), Error> {
.service(web::board::board) .service(web::board::board)
.service(web::board_catalog::board_catalog) .service(web::board_catalog::board_catalog)
.service(web::index::index) .service(web::index::index)
.service(web::ip_posts::ip_posts) .service(web::captcha::captcha)
.service(web::edit_posts::edit_posts) .service(web::edit_posts::edit_posts)
.service(web::ip_posts::ip_posts)
.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)

Zobrazit soubor

@ -19,7 +19,6 @@ use crate::{
ban_response, ban_response,
tcx::{account_from_auth_opt, ip_from_req}, tcx::{account_from_auth_opt, ip_from_req},
}, },
CAPTCHA,
}; };
#[derive(MultipartForm)] #[derive(MultipartForm)]
@ -95,36 +94,23 @@ pub async fn create_post(
None => None, None => None,
}; };
let difficulties = ["easy", "medium", "hard"]; if (thread.is_none() && board.config.0.thread_captcha != "off")
|| (thread.is_some() && board.config.0.reply_captcha != "off")
let difficulty = if thread.is_none() { {
if difficulties.contains(&board.config.0.thread_captcha.as_str()) {
Some(board.config.0.thread_captcha.clone())
} else {
None
}
} else if difficulties.contains(&board.config.0.reply_captcha.as_str()) {
Some(board.config.0.reply_captcha.clone())
} else {
None
};
if let Some(difficulty) = difficulty {
let board = board.id.clone(); let board = board.id.clone();
let id = form let id = form
.captcha_id .captcha_id
.ok_or(NekrochanError::RequiredCaptchaError)? .ok_or(NekrochanError::RequiredCaptchaError)?
.0; .0;
let key = (board, difficulty, id); let key = format!("captcha:{board}:{id}");
let solution = form let solution = form
.captcha_solution .captcha_solution
.ok_or(NekrochanError::RequiredCaptchaError)?; .ok_or(NekrochanError::RequiredCaptchaError)?;
let actual_solution = CAPTCHA let actual_solution: String = ctx.cache().get_del(key).await?;
.write()?
.remove(&key)
.ok_or(NekrochanError::SolvedCaptchaError)?;
if solution.trim() != actual_solution { if solution.trim() != actual_solution {
return Err(NekrochanError::IncorrectCaptchaError); return Err(NekrochanError::IncorrectCaptchaError);

55
src/web/captcha.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,55 @@
use ::captcha::{gen, Difficulty};
use actix_web::{
get,
web::{Data, Json, Query},
};
use redis::AsyncCommands;
use serde::{Deserialize, Serialize};
use sha256::digest;
use crate::{ctx::Ctx, db::models::Board, error::NekrochanError};
#[derive(Deserialize)]
pub struct CaptchaQuery {
pub board: String,
pub reply: bool,
}
#[derive(Serialize)]
pub struct CaptchaResponse {
pub png: String,
pub id: String,
}
#[get("/captcha")]
pub async fn captcha(
ctx: Data<Ctx>,
Query(query): Query<CaptchaQuery>,
) -> Result<Json<CaptchaResponse>, NekrochanError> {
let board = Board::read(&ctx, query.board.clone())
.await?
.ok_or(NekrochanError::BoardNotFound(query.board))?;
let captcha = match board.config.thread_captcha.as_str() {
"easy" => gen(Difficulty::Easy),
"medium" => gen(Difficulty::Medium),
"hard" => gen(Difficulty::Hard),
_ => return Err(NekrochanError::NoCaptchaError),
};
// >NOOOOOOOO YOU NEED TO MAKE A NEW ERROR TYPE FOR THIS ERROR THAT CAN ONLY HAPPEN ONCE IN THE CODE NOOOOOOOOOO
let png = captcha.as_base64().ok_or(NekrochanError::NoCaptchaError)?;
let board = board.id;
let id = digest(png.as_bytes());
let key = format!("captcha:{board}:{id}");
let solution = captcha.chars_as_string();
ctx.cache().set(&key, solution).await?;
ctx.cache().expire(&key, 600).await?;
let res = CaptchaResponse { png, id };
Ok(Json(res))
}

Zobrazit soubor

@ -32,6 +32,7 @@ pub async fn login_get(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse,
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct LogInForm { pub struct LogInForm {
username: String, username: String,
#[serde(rename = "account_password")]
password: String, password: String,
} }

Zobrazit soubor

@ -1,11 +1,10 @@
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder};
use askama::Template;
pub mod actions; pub mod actions;
pub mod board; pub mod board;
pub mod board_catalog; pub mod board_catalog;
pub mod captcha;
pub mod edit_posts; pub mod edit_posts;
pub mod index; pub mod index;
pub mod ip_posts;
pub mod login; pub mod login;
pub mod logout; pub mod logout;
pub mod news; pub mod news;
@ -14,7 +13,9 @@ pub mod overboard_catalog;
pub mod staff; pub mod staff;
pub mod tcx; pub mod tcx;
pub mod thread; pub mod thread;
pub mod ip_posts;
use actix_web::{http::StatusCode, HttpRequest, HttpResponse, HttpResponseBuilder};
use askama::Template;
use self::tcx::TemplateCtx; use self::tcx::TemplateCtx;
use crate::{ctx::Ctx, db::models::Ban, error::NekrochanError, filters}; use crate::{ctx::Ctx, db::models::Ban, error::NekrochanError, filters};

Zobrazit soubor

@ -1,5 +1,4 @@
use actix_web::HttpRequest; use actix_web::HttpRequest;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use redis::AsyncCommands; use redis::AsyncCommands;
use std::{ use std::{
collections::HashSet, collections::HashSet,
@ -17,7 +16,6 @@ pub struct TemplateCtx {
pub account: Option<String>, pub account: Option<String>,
pub perms: PermissionWrapper, pub perms: PermissionWrapper,
pub name: Option<String>, pub name: Option<String>,
pub password: String,
pub ip: IpAddr, pub ip: IpAddr,
pub yous: HashSet<String>, pub yous: HashSet<String>,
} }
@ -35,16 +33,6 @@ impl TemplateCtx {
}; };
let name = req.cookie("name").map(|cookie| cookie.value().into()); let name = req.cookie("name").map(|cookie| cookie.value().into());
let password_cookie = req.cookie("password").map(|cookie| cookie.value().into());
let password: String = match password_cookie {
Some(password) => password,
None => thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect(),
};
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?;
@ -56,7 +44,6 @@ impl TemplateCtx {
boards, boards,
perms, perms,
name, name,
password,
ip, ip,
yous, yous,
account, account,

25
static/js/captcha.js Normální soubor
Zobrazit soubor

@ -0,0 +1,25 @@
$(function () {
$("#get-captcha").click(function () {
let btn = $(this);
let board = btn.attr("data-board");
let reply = btn.attr("data-reply");
let req_url = `/captcha?board=${board}&reply=${reply}`;
btn.text("Získat CAPTCHA");
btn.prop("disabled", true);
btn.addClass("loading");
$.get(req_url, function (data, _) {
try {
$("#captcha").replaceWith(`<img id="captcha" src="data:image/png;base64,${data.png}">`);
$("#captcha-id").prop("value", data.id);
} catch {
btn.append(" [Chyba]");
}
btn.prop("disabled", false);
btn.removeClass("loading");
});
});
});

48
static/js/password.js Normální soubor
Zobrazit soubor

@ -0,0 +1,48 @@
$(function () {
let password = get_cookie("password");
if (password === "") {
password = generate_password();
set_cookie("password", password);
}
$('input[name="password"]').prop("value", password);
});
function generate_password() {
let chars =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let password_length = 8;
let password = "";
for (let i = 0; i <= password_length; i++) {
let random_number = Math.floor(Math.random() * chars.length);
password += chars.substring(random_number, random_number + 1);
}
return password;
}
function get_cookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function set_cookie(cname, cvalue) {
document.cookie = `${cname}=${cvalue};path=/`;
}

Zobrazit soubor

@ -160,12 +160,6 @@ summary {
padding: 8px; padding: 8px;
} }
.captcha {
width: 100%;
image-rendering: pixelated;
border: 1px solid var(--input-border);
}
.main { .main {
margin: 8px; margin: 8px;
} }
@ -223,7 +217,6 @@ summary {
.form-table .button, .form-table .button,
.full-width { .full-width {
display: block;
width: 100%; width: 100%;
} }
@ -479,4 +472,17 @@ summary {
.loading { .loading {
opacity: 0.5; opacity: 0.5;
cursor: wait;
}
img#captcha {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
display: block;
width: 100%;
image-rendering: pixelated;
border: 1px solid var(--input-border);
margin: 0px auto;
} }

Zobrazit soubor

@ -11,9 +11,11 @@
<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> <script src="/static/js/expand-image.js"></script>
<script src="/static/js/password.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</head> </head>
<body id="top"> <body>
<div id="top"></div>
<div class="board-links header"> <div class="board-links header">
<span class="link-group"><a href="/">domov</a></span> <span class="link-group"><a href="/">domov</a></span>
{% call board_links::board_links() %} {% call board_links::board_links() %}
@ -36,7 +38,7 @@
</div> </div>
<div class="main"> <div class="main">
{% block content %}{% endblock %} {% block content %}{% endblock %}
<div id="bottom" class="footer"> <div class="footer">
<div class="box inline-block"> <div class="box inline-block">
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a> <a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
<br> <br>
@ -44,5 +46,6 @@
</div> </div>
</div> </div>
</div> </div>
<div id="bottom"></div>
</body> </body>
</html> </html>

Zobrazit soubor

@ -7,6 +7,7 @@
{% block theme %}{{ board.config.0.board_theme }}{% endblock %} {% block theme %}{{ board.config.0.board_theme }}{% endblock %}
{% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %} {% block title %}/{{ board.id }}/ - {{ board.name }}{% endblock %}
{% block scripts %}<script src="/static/js/captcha.js"></script>{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">

Zobrazit soubor

@ -13,7 +13,7 @@
</tr> </tr>
<tr> <tr>
<td class="label">Heslo</td> <td class="label">Heslo</td>
<td><input name="password" type="password" required=""></td> <td><input name="account_password" type="password" required=""></td>
</tr> </tr>
<tr> <tr>
<td colspan="2"><input class="button" type="submit" value="Přihlásit se"></td> <td colspan="2"><input class="button" type="submit" value="Přihlásit se"></td>

Zobrazit soubor

@ -29,14 +29,7 @@
</tr> </tr>
<tr> <tr>
<td class="label">Heslo</td> <td class="label">Heslo</td>
<td> <td><input name="password" type="password"></td>
<input
name="password"
type="text"
autocomplete="new-password"
value="{{ tcx.password }}"
>
</td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">

Zobrazit soubor

@ -52,33 +52,40 @@
</tr> </tr>
<tr> <tr>
<td class="label">Heslo <span class="small">(na odstranění)</span></td> <td class="label">Heslo <span class="small">(na odstranění)</span></td>
<td> <td><input name="password" type="password" required=""></td>
<input name="password" type="text" autocomplete="new-password" value="{{ tcx.password }}" required="">
</td>
</tr> </tr>
{% if !(tcx.perms.bypass_captcha() || tcx.perms.owner()) %} {% if !(tcx.perms.bypass_captcha() || tcx.perms.owner()) %}
{% let difficulty %}
{% if reply %} {% if reply %}
{% if let Some((id, base64)) = board.reply_captcha() %} {% let difficulty = board.config.0.reply_captcha.as_str() %}
<tr>
<td class="label">CAPTCHA</td>
<td>
<img class="captcha" src="data:image/png;base64,{{ base64 }}">
<input name="captcha_id" type="hidden" value="{{ id }}">
<input name="captcha_solution" type="text" placeholder="Řešení" required="">
</td>
</tr>
{% endif %}
{% else %} {% else %}
{% if let Some((id, base64)) = board.thread_captcha() %} {% let difficulty = board.config.0.thread_captcha.as_str() %}
{% endif %}
{% if (!reply && board.config.0.thread_captcha != "off") || (reply && board.config.0.reply_captcha != "off") %}
<noscript>
<tr> <tr>
<td class="label">CAPTCHA</td> <td colspan="2">
<td> <div class="reply-mode">CAPTCHA vyžaduje JavaScript</div>
<img class="captcha" src="data:image/png;base64,{{ base64 }}"> </td>
<input name="captcha_id" type="hidden" value="{{ id }}"> </tr>
<input name="captcha_solution" type="text" placeholder="Řešení" required=""> </noscript>
<tr>
<td class="label">
CAPTCHA<br>
<span class="small">(vyprší za 10 min.)</span>
</td>
<td>
<div class="input-wrapper">
<table class="full-width">
<tr><td><button id="get-captcha" type="button" class="button" data-board="{{ board.id }}" data-reply="{{ reply }}">Získat CAPTCHA</button></td></tr>
<tr><td><div id="captcha"></div></tr>
<input id="captcha-id" name="captcha_id" type="hidden" required="">
<tr><td><input name="captcha_solution" type="text" placeholder="Řešení" required=""></tr></td>
</table>
</div>
</td> </td>
</tr> </tr>
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
<tr> <tr>

Zobrazit soubor

@ -45,7 +45,7 @@
</tr> </tr>
<tr> <tr>
<td class="label">Heslo</td> <td class="label">Heslo</td>
<td><input name="password" type="password" required=""></td> <td><input name="account_password" type="password" required=""></td>
</tr> </tr>
<tr> <tr>
<td colspan="2"><input class="button" type="submit" value="Vytvořit účet"></td> <td colspan="2"><input class="button" type="submit" value="Vytvořit účet"></td>

Zobrazit soubor

@ -76,10 +76,10 @@
<option value="off" {% if board.config.0.thread_captcha == "off" %}selected="selected"{% endif %}> <option value="off" {% if board.config.0.thread_captcha == "off" %}selected="selected"{% endif %}>
Žádná Žádná
</option> </option>
<option value="medium" {% if board.config.0.thread_captcha == "medium" %}selected="selected"{% endif %}> <option value="easy" {% if board.config.0.thread_captcha == "easy" %}selected="selected"{% endif %}>
Lehká Lehká
</option> </option>
<option value="easy" {% if board.config.0.thread_captcha == "easy" %}selected="selected"{% endif %}> <option value="medium" {% if board.config.0.thread_captcha == "medium" %}selected="selected"{% endif %}>
Střední Střední
</option> </option>
<option value="hard" {% if board.config.0.thread_captcha == "hard" %}selected="selected"{% endif %}> <option value="hard" {% if board.config.0.thread_captcha == "hard" %}selected="selected"{% endif %}>
@ -96,10 +96,10 @@
<option value="off" {% if board.config.0.reply_captcha == "off" %}selected="selected"{% endif %}> <option value="off" {% if board.config.0.reply_captcha == "off" %}selected="selected"{% endif %}>
Žádná Žádná
</option> </option>
<option value="medium" {% if board.config.0.reply_captcha == "medium" %}selected="selected"{% endif %}> <option value="easy" {% if board.config.0.reply_captcha == "easy" %}selected="selected"{% endif %}>
Lehká Lehká
</option> </option>
<option value="easy" {% if board.config.0.reply_captcha == "easy" %}selected="selected"{% endif %}> <option value="medium" {% if board.config.0.reply_captcha == "medium" %}selected="selected"{% endif %}>
Střední Střední
</option> </option>
<option value="hard" {% if board.config.0.reply_captcha == "hard" %}selected="selected"{% endif %}> <option value="hard" {% if board.config.0.reply_captcha == "hard" %}selected="selected"{% endif %}>

Zobrazit soubor

@ -6,7 +6,7 @@
{% 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/update-thread.js"></script>{% endblock %} {% block scripts %}<script src="/static/js/captcha.js"></script>{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container">