use std::collections::HashMap; use fancy_regex::{Captures, Regex}; use lazy_static::lazy_static; use sqlx::query_as; 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"(?m)^>((?!>\d+|>>/\w+(/\d*)?|>>#/).*)") .unwrap(); pub static ref ORANGETEXT_REGEX: Regex = Regex::new(r"(?m)^<(.+)").unwrap(); pub static ref REDTEXT_REGEX: Regex = Regex::new(r"(?m)==(.+?)==").unwrap(); pub static ref BLUETEXT_REGEX: Regex = Regex::new(r"(?m)--(.+?)--").unwrap(); pub static ref GLOWTEXT_REGEX: Regex = Regex::new(r"(?m)\%\%(.+?)\%\%").unwrap(); pub static ref UH_OH_TEXT_REGEX: Regex = Regex::new(r"(?m)\(\(\((.+?)\)\)\)").unwrap(); pub static ref SPOILER_REGEX: Regex = Regex::new(r"(?m)\|\|([\s\S]+?)\|\|").unwrap(); pub static ref URL_REGEX: Regex = Regex::new(r"https?\://[^\s<>\[\]{}|\\^]+").unwrap(); } pub fn parse_name( ctx: &Ctx, perms: &PermissionWrapper, anon_name: &str, name: &str, ) -> Result<(String, Option, Option), NekrochanError> { let captures = match NAME_REGEX.captures(name)? { Some(captures) => captures, None => 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)); } fn capcode_fallback(owner: bool) -> Option { if owner { Some("Admin".into()) } else { Some("Uklízeč".into()) } } 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() { capcode_fallback(perms.owner()) } else { if capcode.len() > 32 { return Err(NekrochanError::CapcodeFormatError); } Some(capcode) } } None => capcode_fallback(perms.owner()), }, None => None, }; Ok((name, tripcode, capcode)) } pub async fn markup( ctx: &Ctx, board: &String, op: Option, text: &str, ) -> Result { let text = escape_html(text); 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 id = match id_raw.parse() { Ok(id) => id, Err(_) => return format!(">>{id_raw}"), }; let post = quoted_posts.get(&id); match post { Some(post) => format!( ">>{}{}", post.post_url(), post.id, if op == Some(post.id) { " (OP)" } else { "" } ), None => format!(">>{id}"), } }); 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}") }); Ok(text.to_string()) } 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.unwrap()[1]; let id = match id_raw.parse() { Ok(id) => id, Err(_) => continue, }; quoted_ids.push(id); } if quoted_ids.is_empty() { return Ok(HashMap::new()); } let in_list = quoted_ids .iter() .map(|id| id.to_string()) .collect::>() .join(","); let quoted_posts = query_as(&format!( "SELECT * FROM posts_{} WHERE id IN ({})", board, in_list )) .fetch_all(ctx.db()) .await? .into_iter() .map(|post: Post| (post.id, post)) .collect::>(); Ok(quoted_posts) }