nekrochan/src/markup.rs

230 řádky
6.9 KiB
Rust

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?\:&#x2F;&#x2F;[^\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<String>, Option<String>), 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<String>,
op: Option<i64>,
text: &str,
) -> Result<(String, Vec<Post>), 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!("<span class=\"dead-quote\">&gt;&gt;{id_raw}</span>");
};
let post = quoted_posts.get(&id);
if let Some(post) = post {
format!(
"<a class=\"quote\" href=\"{}\">&gt;&gt;{}</a>{}",
post.post_url(),
post.id,
if op == Some(post.id) {
" <span class=\"small\">(OP)</span>"
} else {
""
}
)
} else {
format!("<span class=\"dead-quote\">&gt;&gt;{id}</span>")
}
});
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, "<span class=\"greentext\">&gt;$1</span>");
let text = ORANGETEXT_REGEX.replace_all(&text, "<span class=\"orangetext\">&lt;$1</span>");
let text = REDTEXT_REGEX.replace_all(&text, "<span class=\"redtext\">$1</span>");
let text = BLUETEXT_REGEX.replace_all(&text, "<span class=\"bluetext\">$1</span>");
let text = GLOWTEXT_REGEX.replace_all(&text, "<span class=\"glowtext\">$1</span>");
let text = SPOILER_REGEX.replace_all(&text, "<span class=\"spoiler\">$1</span>");
let text = UH_OH_TEXT_REGEX.replace_all(&text, |captures: &Captures| {
format!(
"<span class=\"uh-oh-text\">((( {} )))</span>",
captures[1].trim()
)
});
let text = URL_REGEX.replace_all(&text, |captures: &Captures| {
let url = &captures[0];
format!("<a rel=\"nofollow\" href=\"{url}\">{url}</a>")
});
let text = if perms.owner() || perms.jannytext() {
JANNYTEXT_REGEX.replace_all(&text, "<span class=\"jannytext\">$1</span>")
} else {
text
};
Ok((text.to_string(), quoted_posts))
}
fn escape_html(text: &str) -> String {
text.replace('&', "&amp;")
.replace('\'', "&#39;")
.replace('/', "&#x2F;")
.replace('`', "&#x60;")
.replace('=', "&#x3D;")
.replace('<', "&lt;")
.replace('>', "&gt;")
.replace('"', "&quot;")
}
async fn get_quoted_posts(
ctx: &Ctx,
board: &String,
text: &str,
) -> Result<HashMap<i64, Post>, NekrochanError> {
let mut quoted_ids: Vec<i64> = Vec::new();
for quote in QUOTE_REGEX.captures_iter(text) {
let id_raw = &quote[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::<Vec<_>>()
.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::<HashMap<_, _>>();
Ok(quoted_posts)
}