use lazy_static::lazy_static; use regex::{Captures, Regex}; use sqlx::query_as; use std::collections::HashMap; use crate::{ ctx::Ctx, db::models::Post, error::NekrochanError, perms::PermissionWrapper, trip::{secure_tripcode, tripcode}, }; lazy_static! { pub static ref NAME_REGEX: Regex = Regex::new(r"^([^#].*?)?(?:(##([^ ].*?)|#([^#].*?)))?(##( .*?)?)?$").unwrap(); pub static ref QUOTE_REGEX: Regex = Regex::new(r">>(\d+)").unwrap(); pub static ref GREENTEXT_REGEX: Regex = Regex::new(r"(?mR)^>(.*)$").unwrap(); pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?mR)^<(.*)$").unwrap(); pub static ref REDTEXT_REGEX: Regex = Regex::new(r"==(.+?)==").unwrap(); pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"--(.+?)--").unwrap(); pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"\%\%(.+?)\%\%").unwrap(); pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"\(\(\((.+?)\)\)\)").unwrap(); pub static ref SPOILER_REGEX: Regex = Regex::new(r"\|\|([\s\S]+?)\|\|").unwrap(); pub static ref URL_REGEX: Regex = Regex::new(r"https?\://[^\s<>\[\]{}|\\^]+").unwrap(); pub static ref JANNYTEXT_REGEX: Regex = Regex::new(r"##(.+?)##").unwrap(); } pub fn parse_name( ctx: &Ctx, perms: &PermissionWrapper, anon_name: &str, name: &str, ) -> Result<(String, Option, Option), NekrochanError> { let Some(captures) = NAME_REGEX.captures(name) else { return Ok((anon_name.to_owned(), None, None)); }; let name = match captures.get(1) { Some(name) => { let name = name.as_str().to_owned(); if name.len() > 32 { return Err(NekrochanError::PostNameFormatError); } name } None => anon_name.to_owned(), }; let tripcode = match captures.get(2) { Some(_) => { let strip = captures.get(3); let itrip = captures.get(4); if let Some(strip) = strip { let trip = secure_tripcode(strip.as_str(), &ctx.cfg.secrets.secure_trip); Some(format!("!!{trip}")) } else if let Some(itrip) = itrip { let trip = tripcode(itrip.as_str()); Some(format!("!{trip}")) } else { None } } None => None, }; if !(perms.owner() || perms.capcodes()) { return Ok((name, tripcode, None)); } let capcode = match captures.get(5) { Some(_) => match captures.get(6) { Some(capcode) => { let capcode: String = capcode.as_str().trim().into(); if capcode.is_empty() || !(perms.owner() || perms.custom_capcodes()) { Some(capcode_fallback(perms.owner())) } else { if capcode.len() > 32 { return Err(NekrochanError::CapcodeFormatError); } Some(capcode) } } None => Some(capcode_fallback(perms.owner())), }, None => None, }; Ok((name, tripcode, capcode)) } fn capcode_fallback(owner: bool) -> String { if owner { "Admin".into() } else { "Uklízeč".into() } } pub async fn markup( ctx: &Ctx, perms: &PermissionWrapper, board: Option, op: Option, text: &str, ) -> Result<(String, Vec), NekrochanError> { let text = escape_html(text); let (text, quoted_posts) = if let Some(board) = board { let quoted_posts = get_quoted_posts(ctx, &board, &text).await?; let text = QUOTE_REGEX.replace_all(&text, |captures: &Captures| { let id_raw = &captures[1]; let Ok(id) = id_raw.parse() else { return format!(">>{id_raw}"); }; let post = quoted_posts.get(&id); if let Some(post) = post { format!( ">>{}{}", post.post_url(), post.id, if op == Some(post.id) { " (OP)" } else { "" } ) } else { format!(">>{id}") } }); let quoted_posts = quoted_posts .into_values() .filter(|post| op == Some(post.thread.unwrap_or(post.id))) .collect(); (text.to_string(), quoted_posts) } else { (text, Vec::new()) }; let text = GREENTEXT_REGEX.replace_all(&text, ">$1"); let text = ORANGETEXT_REGEX.replace_all(&text, "<$1"); let text = REDTEXT_REGEX.replace_all(&text, "$1"); let text = BLUETEXT_REGEX.replace_all(&text, "$1"); let text = GLOWTEXT_REGEX.replace_all(&text, "$1"); let text = SPOILER_REGEX.replace_all(&text, "$1"); let text = UH_OH_TEXT_REGEX.replace_all(&text, |captures: &Captures| { format!( "((( {} )))", captures[1].trim() ) }); let text = URL_REGEX.replace_all(&text, |captures: &Captures| { let url = &captures[0]; format!("{url}") }); let text = if perms.owner() || perms.jannytext() { JANNYTEXT_REGEX.replace_all(&text, "$1") } else { text }; Ok((text.to_string(), quoted_posts)) } fn escape_html(text: &str) -> String { text.replace('&', "&") .replace('\'', "'") .replace('/', "/") .replace('`', "`") .replace('=', "=") .replace('<', "<") .replace('>', ">") .replace('"', """) } async fn get_quoted_posts( ctx: &Ctx, board: &String, text: &str, ) -> Result, NekrochanError> { let mut quoted_ids: Vec = Vec::new(); for quote in QUOTE_REGEX.captures_iter(text) { let id_raw = "e[1]; let Ok(id) = id_raw.parse() else { continue }; quoted_ids.push(id); } if quoted_ids.is_empty() { return Ok(HashMap::new()); } let in_list = quoted_ids .iter() .map(std::string::ToString::to_string) .collect::>() .join(","); let quoted_posts = query_as(&format!( "SELECT * FROM posts_{board} WHERE id IN ({in_list})" )) .fetch_all(ctx.db()) .await? .into_iter() .map(|post: Post| (post.id, post)) .collect::>(); Ok(quoted_posts) }