325 řádky
8.8 KiB
Rust
325 řádky
8.8 KiB
Rust
use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm};
|
|
use actix_web::{
|
|
cookie::Cookie, http::StatusCode, post, web::Data, HttpRequest, HttpResponse,
|
|
HttpResponseBuilder,
|
|
};
|
|
use chrono::{Duration, Utc};
|
|
use pwhash::bcrypt::hash;
|
|
use redis::AsyncCommands;
|
|
use sha256::digest;
|
|
use std::{collections::HashSet, net::IpAddr};
|
|
|
|
use crate::{
|
|
ctx::Ctx,
|
|
db::models::{Ban, Board, File, Post},
|
|
error::NekrochanError,
|
|
markup::{markup, parse_name},
|
|
perms::PermissionWrapper,
|
|
web::{
|
|
ban_response,
|
|
tcx::{account_from_auth_opt, ip_from_req},
|
|
},
|
|
};
|
|
|
|
#[derive(MultipartForm)]
|
|
pub struct PostForm {
|
|
pub board: Text<String>,
|
|
pub thread: Option<Text<i64>>,
|
|
#[multipart(rename = "post_name")]
|
|
pub name: Text<String>,
|
|
pub email: Text<String>,
|
|
pub content: Text<String>,
|
|
#[multipart(rename = "files[]")]
|
|
pub files: Vec<TempFile>,
|
|
pub spoiler_files: Option<Text<String>>,
|
|
#[multipart(rename = "post_password")]
|
|
pub password: Text<String>,
|
|
pub captcha_id: Option<Text<String>>,
|
|
pub captcha_solution: Option<Text<String>>,
|
|
}
|
|
|
|
#[post("/actions/create-post")]
|
|
pub async fn create_post(
|
|
ctx: Data<Ctx>,
|
|
req: HttpRequest,
|
|
MultipartForm(form): MultipartForm<PostForm>,
|
|
) -> Result<HttpResponse, NekrochanError> {
|
|
let perms = match account_from_auth_opt(&ctx, &req).await? {
|
|
Some(account) => account.perms(),
|
|
None => PermissionWrapper::new(0, false),
|
|
};
|
|
|
|
let (ip, country) = ip_from_req(&req)?;
|
|
|
|
let board = form.board.0;
|
|
let board = Board::read(&ctx, board.clone())
|
|
.await?
|
|
.ok_or(NekrochanError::BoardNotFound(board))?;
|
|
|
|
if let Some(ban) = Ban::read(&ctx, board.id.clone(), ip).await? {
|
|
if !(perms.owner() || perms.bypass_bans()) {
|
|
return ban_response(&ctx, &req, ban).await;
|
|
}
|
|
}
|
|
|
|
if board.config.0.locked && !(perms.owner() || perms.bypass_board_lock()) {
|
|
return Err(NekrochanError::BoardLockError(board.id.clone()));
|
|
}
|
|
|
|
let mut bump = true;
|
|
let mut noko = ctx.cfg.site.default_noko;
|
|
|
|
let thread = match form.thread {
|
|
Some(Text(thread)) => {
|
|
let thread = Post::read(&ctx, board.id.clone(), thread)
|
|
.await?
|
|
.ok_or(NekrochanError::PostNotFound(board.id.clone(), thread))?;
|
|
|
|
if thread.thread.is_some() {
|
|
return Err(NekrochanError::IsReplyError);
|
|
}
|
|
|
|
if thread.locked && !(perms.owner() || perms.bypass_thread_lock()) {
|
|
return Err(NekrochanError::ThreadLockError);
|
|
}
|
|
|
|
if thread.replies >= board.config.0.reply_limit {
|
|
return Err(NekrochanError::ReplyLimitError);
|
|
}
|
|
|
|
if thread.bumps >= board.config.0.bump_limit {
|
|
bump = false;
|
|
}
|
|
|
|
Some(thread)
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
if !(perms.owner() || perms.bypass_captcha())
|
|
&& ((thread.is_none() && board.config.0.thread_captcha != "off")
|
|
|| (thread.is_some() && board.config.0.reply_captcha != "off"))
|
|
{
|
|
let board = board.id.clone();
|
|
|
|
let id = form
|
|
.captcha_id
|
|
.ok_or(NekrochanError::RequiredCaptchaError)?
|
|
.0;
|
|
|
|
if id.is_empty() {
|
|
return Err(NekrochanError::RequiredCaptchaError);
|
|
}
|
|
|
|
let key = format!("captcha:{board}:{id}");
|
|
|
|
let solution = form
|
|
.captcha_solution
|
|
.ok_or(NekrochanError::RequiredCaptchaError)?;
|
|
|
|
let actual_solution: Option<String> = ctx.cache().get_del(key).await?;
|
|
let actual_solution = actual_solution.ok_or(NekrochanError::InvalidCaptchaError)?;
|
|
|
|
if solution.trim() != actual_solution {
|
|
return Err(NekrochanError::IncorrectCaptchaError);
|
|
}
|
|
}
|
|
|
|
let name_raw = form.name.trim();
|
|
let (name, tripcode, capcode) = parse_name(&ctx, &perms, &board.config.0.anon_name, name_raw)?;
|
|
|
|
let email_raw = form.email.trim();
|
|
|
|
let email = if email_raw.is_empty() {
|
|
None
|
|
} else {
|
|
if email_raw.len() > 256 {
|
|
return Err(NekrochanError::EmailFormatError);
|
|
}
|
|
|
|
let email_lower = email_raw.to_lowercase();
|
|
|
|
if email_lower == "sage" {
|
|
bump = false;
|
|
}
|
|
|
|
if !ctx.cfg.site.default_noko && email_lower == "noko" {
|
|
noko = true
|
|
}
|
|
|
|
if ctx.cfg.site.default_noko {
|
|
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())
|
|
};
|
|
|
|
let content_nomarkup = form.content.0.trim().to_owned();
|
|
|
|
if content_nomarkup.is_empty() && (thread.is_none() && board.config.0.require_thread_content)
|
|
|| (thread.is_some() && board.config.0.require_reply_content)
|
|
{
|
|
return Err(NekrochanError::NoContentError);
|
|
}
|
|
|
|
if content_nomarkup.len() > 10000 {
|
|
return Err(NekrochanError::ContentFormatError);
|
|
}
|
|
|
|
let content = markup(
|
|
&ctx,
|
|
&perms,
|
|
Some(board.id.clone()),
|
|
thread.as_ref().map(|t| t.id),
|
|
&content_nomarkup,
|
|
)
|
|
.await?;
|
|
|
|
if board.config.antispam {
|
|
check_spam(&ctx, &board, ip, content_nomarkup.clone()).await?;
|
|
}
|
|
|
|
if form.files.len() > board.config.0.file_limit {
|
|
return Err(NekrochanError::FileLimitError(board.config.0.file_limit));
|
|
}
|
|
|
|
let mut files = Vec::new();
|
|
|
|
for file in form.files {
|
|
if file.size == 0 {
|
|
continue;
|
|
}
|
|
|
|
let spoiler = form.spoiler_files.is_some();
|
|
let file = File::new(&ctx.cfg, file, spoiler, true).await?;
|
|
|
|
files.push(file);
|
|
}
|
|
|
|
if content_nomarkup.is_empty() && files.is_empty() {
|
|
return Err(NekrochanError::EmptyPostError);
|
|
}
|
|
|
|
let password_raw = form.password.trim();
|
|
|
|
if password_raw.len() < 8 {
|
|
return Err(NekrochanError::PasswordFormatError);
|
|
}
|
|
|
|
let password = hash(password_raw)?;
|
|
let thread_id = thread.as_ref().map(|t| t.id);
|
|
|
|
let post = Post::create(
|
|
&ctx,
|
|
&board,
|
|
thread_id,
|
|
name,
|
|
tripcode,
|
|
capcode,
|
|
email,
|
|
content,
|
|
content_nomarkup,
|
|
files,
|
|
password,
|
|
country,
|
|
ip,
|
|
bump,
|
|
)
|
|
.await?;
|
|
|
|
let ts = thread.as_ref().map_or_else(
|
|
|| post.created.timestamp_micros(),
|
|
|thread| thread.created.timestamp_micros(),
|
|
);
|
|
|
|
let hash_input = format!("{}:{}:{}", ip, ts, ctx.cfg.secrets.user_id);
|
|
let user_hash = digest(hash_input);
|
|
let user_id = user_hash[..6].to_owned();
|
|
|
|
post.update_user_id(&ctx, user_id).await?;
|
|
|
|
let mut res = HttpResponseBuilder::new(StatusCode::SEE_OTHER);
|
|
|
|
let name_cookie = Cookie::build("name", name_raw).path("/").finish();
|
|
let password_cookie = Cookie::build("password", password_raw).path("/").finish();
|
|
|
|
res.cookie(name_cookie);
|
|
res.cookie(password_cookie);
|
|
|
|
let res = if noko {
|
|
res.append_header(("Location", post.post_url().as_str()))
|
|
.finish()
|
|
} else {
|
|
res.append_header(("Location", format!("/boards/{}", post.board).as_str()))
|
|
.finish()
|
|
};
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
pub async fn check_spam(
|
|
ctx: &Ctx,
|
|
board: &Board,
|
|
ip: IpAddr,
|
|
content_nomarkup: String,
|
|
) -> Result<(), NekrochanError> {
|
|
let ip_key = format!("by_ip:{ip}");
|
|
let content_key = format!("by_content:{}", digest(content_nomarkup));
|
|
|
|
let antispam_ip = (Utc::now() - Duration::seconds(board.config.antispam_ip)).timestamp_micros();
|
|
let antispam_content =
|
|
(Utc::now() - Duration::seconds(board.config.antispam_content)).timestamp_micros();
|
|
let antispam_both =
|
|
(Utc::now() - Duration::seconds(board.config.antispam_both)).timestamp_micros();
|
|
|
|
let ip_posts: HashSet<String> = ctx
|
|
.cache()
|
|
.zrangebyscore(&ip_key, antispam_ip, "+inf")
|
|
.await?;
|
|
let content_posts: HashSet<String> = ctx
|
|
.cache()
|
|
.zrangebyscore(&content_key, antispam_content, "+inf")
|
|
.await?;
|
|
|
|
let ip_posts2: HashSet<String> = ctx
|
|
.cache()
|
|
.zrangebyscore(&ip_key, antispam_both, "+inf")
|
|
.await?;
|
|
let content_posts2: HashSet<String> = ctx
|
|
.cache()
|
|
.zrangebyscore(&content_key, antispam_both, "+inf")
|
|
.await?;
|
|
|
|
let both_posts = ip_posts2.intersection(&content_posts2);
|
|
|
|
if !ip_posts.is_empty() {
|
|
return Err(NekrochanError::FloodError);
|
|
}
|
|
|
|
if !content_posts.is_empty() {
|
|
return Err(NekrochanError::FloodError);
|
|
}
|
|
|
|
if both_posts.count() != 0 {
|
|
return Err(NekrochanError::FloodError);
|
|
}
|
|
|
|
Ok(())
|
|
}
|